GitHub Issues

GitHub Issues is the built-in tracker every GitHub repository ships with — a lightweight but powerful way to record bugs, feature requests, questions, and tasks right next to the code they describe. Unlike a commit or a branch, an issue is not a Git object at all: it lives entirely on GitHub’s servers as metadata attached to the repository, fully searchable and filterable, and cross-linkable to commits, pull requests, and other issues. Understanding how issues connect to your everyday Git workflow — how a commit message can automatically close an issue, how issue numbers interact with pull request numbers, how labels and milestones organize a backlog of hundreds of items — turns Issues from a glorified to-do list into the backbone of real project management on GitHub.

Overview: What an Issue Is and How It Fits Into Git and GitHub

An issue is a title, a Markdown body, and a bundle of metadata: state (open or closed), labels, assignees, a milestone, a list of linked pull requests, and a chronological timeline of comments and events. All of that lives in GitHub’s database, not in your repository’s Git history. Nothing about an issue is content-addressed the way a commit, tree, or blob is — an issue has no SHA-1 hash, it isn’t cloned when you run git clone, and it can be edited or deleted after the fact without rewriting any history. It is purely a GitHub-product concept layered on top of the repository, the same way pull requests, Actions workflows, and project boards are.

That said, issues are deliberately woven into the Git side of your work. GitHub scans every commit message and every pull request description for references to issue numbers (#42) and, more specifically, for a small set of “closing keywords.” When one of those references is merged into the repository’s default branch, GitHub automatically transitions the issue to closed and records exactly which commit did it — no separate click required.

Issue numbers are shared with pull requests

A single incrementing counter is shared by issues and pull requests within one repository. If issue #41 was the last thing created, the next issue or the next pull request — whichever is opened first — becomes #42. This is why #42 in a commit message might resolve to either an issue or a PR; GitHub figures out which from context, but you should always double-check the number belongs to what you think it does before you rely on it.

Closing keywords

Any of the words close, closes, closed, fix, fixes, fixed, resolve, resolves, or resolved, written immediately before an issue reference in a commit message or a pull request body, tells GitHub to close that issue the moment the containing commit lands on the default branch (commonly main). Inside the same repository, Fixes #42 is enough. To close an issue in a different repository — including an upstream repository you forked from — you must qualify it: Fixes octocat/example-app#42.

Syntax

Issues are primarily a web-UI feature, but the gh command-line tool (the official GitHub CLI) lets you create, browse, and manage them without leaving the terminal — handy when you’re already there working with Git:

gh issue create [flags]
gh issue list [flags]
gh issue view "<number>"
gh issue close "<number>"
gh issue comment "<number>" --body "<text>"

gh issue create accepts:

Flag Meaning
--title "<text>" The issue title (required unless you pass --web)
--body "<text>" The Markdown body describing the problem or request
--label <name> Apply an existing label; repeat the flag for more than one
--assignee <user> Assign a GitHub username; repeat the flag for more than one
--milestone <name> Attach the issue to an existing milestone
--web Open the browser-based new-issue form instead of terminal prompts

gh issue list accepts:

Flag Meaning
--state <open|closed|all> Filter by state; defaults to open
--label <name> Only show issues carrying this label
--assignee <user> Only show issues assigned to this user
--search "<query>" Raw GitHub search syntax, e.g. "is:open sort:updated-desc"

Examples

Example 1: Filing a bug report from the terminal

gh issue create \
  --title "Login button unresponsive on mobile Safari" \
  --body "Tapping the login button on iOS Safari does nothing until a second tap. Reproduced on iPhone 13, Safari 17." \
  --label bug \
  --assignee dev-jordan

Output:

Creating issue in octocat/example-app

https://github.com/octocat/example-app/issues/42

The CLI prints the URL of the newly created issue, which GitHub assigned the number 42. That number now identifies this issue for the life of the repository — even if the issue is later closed, the number is never reused.

Example 2: Closing an issue automatically through a merged commit

git switch -c fix/login-button-debounce
git add src/components/LoginButton.jsx
git commit -m "fix: debounce login button tap handler

Fixes #42"
git push -u origin fix/login-button-debounce
gh pr create --title "fix: debounce login button tap handler" --body "Fixes #42" --base main

Output:

Creating pull request for fix/login-button-debounce into main in octocat/example-app

https://github.com/octocat/example-app/pull/43

Nothing closes yet — the commit only lives on the feature branch and the pull request is still open. The moment a maintainer merges pull request #43 into main, GitHub parses the merged commit’s message, finds Fixes #42, and flips issue #42 to closed automatically, adding a timeline entry that links straight back to the merge commit.

Example 3: Finding your open bugs

gh issue list --label bug --state open --assignee dev-jordan

Output:

Showing 1 of 1 open issue in octocat/example-app that matches your search

ID   TITLE                                        LABELS  UPDATED
#42  Login button unresponsive on mobile Safari    bug     about 2 hours ago

Combining --label, --state, and --assignee narrows a busy repository’s issue list down to exactly the work relevant to you — the same filtering the “Issues” tab on github.com offers through its search bar.

How It Works Step by Step

  1. gh issue create (or the “New issue” button on github.com) sends a request to GitHub’s API; GitHub allocates the next number in the repository’s shared issue/PR counter and writes a new issue record with state open.
  2. Every time you push commits or open/update a pull request, GitHub re-scans the commit messages and the PR body/title for issue references and closing keywords.
  3. When a pull request containing a closing keyword is merged into the default branch, GitHub’s backend detects the merge event, resolves the referenced number to an issue in the same repository (or the qualified owner/repo#number repository), and updates that issue’s state field to closed.
  4. A timeline event is appended to the issue recording which commit or pull request closed it, so anyone reading the issue later can jump straight to the fix.
  5. None of this touches your local .git directory. git log, git show, and the commit graph are completely unaware that an issue exists or changed state — issue tracking is entirely a GitHub-side overlay on top of the commits you already made.
  6. Closing an issue manually — with gh issue close 42 or the “Close issue” button — skips all of this. No commit or keyword is required; GitHub just flips the state field directly.

Common Mistakes

Mistake: expecting a closing keyword to work before the branch is merged. Writing Fixes #42 in a commit on fix/login-button-debounce does nothing to issue #42 while that branch sits unmerged — the keyword is only honored on the default branch. The fix: don’t be surprised when the issue is still open after you push; it closes automatically once the pull request is merged, not before.

Mistake: referencing an issue number without qualifying the repository. If you’re working in a fork and write Fixes #17, GitHub resolves #17 against your fork, not the upstream project you actually intend to fix — even if upstream also happens to have an issue #17.

# Wrong: closes an issue in YOUR fork, not upstream
git commit -m "fix: correct off-by-one in pagination

Fixes #17"

# Correct: qualify the repository explicitly
git commit -m "fix: correct off-by-one in pagination

Fixes upstream-org/example-app#17"

The fix: always write the fully qualified Fixes upstream-org/example-app#17 form when the issue lives in a different repository than the one receiving the commit.

Mistake: filing a new issue without searching first. On active repositories the same bug is frequently reported multiple times under slightly different titles, scattering discussion and duplicating triage effort. The fix: search existing issues (including closed ones) with gh issue list --search "<keywords>" --state all before opening a new one, and link the duplicate to the original if you find it.

Mistake: leaving issues with no label, milestone, or assignee. An issue with no metadata is invisible to every filtered view of the backlog — it won’t show up under a label search, on a project board column, or in a milestone’s progress bar. The fix: triage every new issue promptly by assigning at least a label and, if it’s planned work, a milestone.

Best Practices

  • Write issue titles as a specific, searchable summary (“Login button unresponsive on mobile Safari”) rather than a vague one (“Bug on login page”).
  • Use closing keywords (Fixes #42, Closes #17) in every pull request body that resolves an issue, so history and traceability stay automatic.
  • Keep one issue per discrete piece of work; split a sprawling issue into several linked issues rather than letting one thread cover unrelated problems.
  • Adopt issue templates (Markdown or YAML files under .github/ISSUE_TEMPLATE/) so bug reports and feature requests consistently capture reproduction steps, environment, and expected behavior.
  • Use labels for orthogonal facets — type (bug, enhancement), priority, area — rather than piling meaning into the title.
  • Reserve milestones for time-boxed or release-scoped work, and use project boards for ongoing, cross-cutting views of the backlog.
  • Search before filing to avoid duplicates, and close duplicates with a comment linking to the canonical issue instead of leaving both open.

Practice Exercises

  1. Using the GitHub web UI or the gh CLI, create an issue titled “Add dark mode toggle to settings page,” apply a label of your choosing, and assign it to yourself. Note the issue number GitHub assigns.
  2. Create a branch, make a commit whose message body includes a closing keyword referencing the issue number from Exercise 1, push the branch, and open a pull request into main. Before merging, check the issue’s page — confirm it is still open. Merge the pull request and confirm the issue closes automatically with a timeline entry pointing at your merge.
  3. Using gh issue list, write a single command that finds every open issue labeled bug assigned to you, sorted by most recently updated. Compare the result to the equivalent search performed in the “Issues” tab on github.com.

Summary

  • An issue is GitHub-database metadata — title, body, state, labels, assignees, milestone, timeline — not a Git object; it has no SHA and isn’t part of git clone.
  • Issues and pull requests share one incrementing number sequence per repository, so #42 could be either.
  • Closing keywords (Fixes, Closes, Resolves) followed by an issue reference in a commit message or PR body auto-close that issue only once merged into the default branch.
  • Cross-repository references need the owner/repo#number form; a bare #number always resolves within the current repository.
  • The gh issue subcommands (create, list, view, close, comment) let you manage issues without leaving the terminal.
  • Good hygiene — specific titles, labels, milestones, templates, and searching before filing — is what makes Issues scale from a single-person to-do list into real project management.