Linking Issues and Pull Requests

When a pull request fixes a bug or ships a feature, the work usually starts life as an issue – a bug report or feature request describing what needs to happen. GitHub lets you connect the two, so anyone reading either one can jump straight to the other, and so merging the fix can automatically close the ticket that asked for it. This lesson covers the closing keywords GitHub looks for in commit messages and pull request text, how to link across repositories, and how to connect an issue and a pull request manually when a keyword does not fit.

Overview / How it works

Issues and pull requests are not part of Git’s object model. They do not exist as blobs, trees, or commits inside the repository – they live in GitHub’s own database as numbered records. Crucially, issues and pull requests in a single repository share one incrementing number sequence, so a repo might have issue #1, pull request #2, issue #3, and so on, all drawing from the same counter. Linking is a text-parsing feature layered on top of that database: whenever you write text into a commit message, a pull request title or description, or a comment, GitHub scans it for patterns that look like issue references.

There are two kinds of reference. A plain reference – just the number, like #482 – creates a two-way cross-reference. GitHub adds an entry to the issue’s timeline saying something like “mentioned this issue in pull request #501”, and a matching entry on the pull request. Nothing closes automatically; it is only a clickable trail of context.

A closing reference uses one of a specific set of keywords – close, closes, closed, fix, fixes, fixed, resolve, resolves, or resolved – immediately followed by an issue number. This tells GitHub that merging the pull request should close that issue. GitHub does not act on this the moment the text is written; it records the intent and shows a linked-issue badge in the pull request sidebar. The issue only actually closes when the pull request is merged into the repository’s default branch (typically main). If the same pull request is merged into some other branch, such as a long-lived release branch, the closing keyword does nothing until that work eventually lands on the default branch through its own merge.

You can link across repositories, including from a fork back to the repository it was forked from, by prefixing the issue number with the owner and repository name: Fixes octo-org/docs-site#77. Without that prefix, GitHub always resolves a bare #77 to an issue or pull request in whichever repository the text was written in – a common source of accidental mislinking when you meant a different repo. Closing a cross-repository issue this way also requires that whoever merges the pull request has permission to close issues in the target repository; otherwise the reference still appears as a link, but the issue is left open.

Besides text-based keywords, GitHub supports a manual link: open an issue, find the Development section in the right-hand sidebar, and choose Link a pull request (the reverse action exists on pull requests too). This creates the same kind of tracked relationship without needing any particular wording, which is useful when the text does not naturally contain a closing phrase, or when you want to connect an issue and pull request after the fact.

Syntax

The general form, placed anywhere in a commit message body, a pull request title, or a pull request description:

git commit -m "fix: 

Closes #"
Keyword group Words GitHub recognizes Effect when the PR merges into the default branch
Close close, closes, closed Closes the referenced issue
Fix fix, fixes, fixed Closes the referenced issue
Resolve resolve, resolves, resolved Closes the referenced issue
Plain reference #123 with no keyword Cross-references only – never closes

For a different repository, insert owner/repo right before the #, e.g. Fixes octo-org/docs-site#77.

Examples

Example 1: Closing an issue automatically on merge

git switch -c fix/session-timeout
git add src/auth/session.js
git commit -m "fix: extend session timeout to 30 minutes

Closes #482"
git push -u origin fix/session-timeout

Output:

Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 512 bytes | 512.00 KiB/s, done.
Total 4 (delta 2), reused 0 (delta 0), pack-reused 0
remote:
remote: Create a pull request for 'fix/session-timeout' on GitHub by visiting:
remote:      https://github.com/acme/webapp/pull/new/fix/session-timeout
remote:
To github.com:acme/webapp.git
 * [new branch]      fix/session-timeout -> fix/session-timeout
branch 'fix/session-timeout' set up to track 'origin/fix/session-timeout'.

The commit body contains Closes #482. As soon as a pull request is opened from this branch, GitHub scans its commits and description, finds the keyword, and shows issue #482 in the pull request’s Development sidebar as “will close when merged”. Nothing closes yet – that only happens once the pull request is merged into main.

Example 2: A plain reference versus a cross-repository close

gh pr create \
  --title "docs: clarify install steps for Windows" \
  --body "See discussion in #201 for background.

Fixes octo-org/docs-site#77"

Output:

Creating pull request for docs-windows-install into main in acme/webapp

https://github.com/acme/webapp/pull/504

This description does two different things. #201 has no keyword in front of it, so it only creates a cross-reference to issue #201 in the same repository – useful for background, but it will not close anything. Fixes octo-org/docs-site#77 uses the owner/repo prefix, so when this pull request merges into main, GitHub attempts to close issue #77 in the separate octo-org/docs-site repository, provided the person merging has permission to close issues there.

Example 3: Starting from a pre-linked branch with the GitHub CLI

gh issue develop 482 --checkout
gh pr create --title "fix: extend session timeout to 30 minutes" --body "Closes #482"

Output:

Created branch 482-session-times-out-too-quickly

https://github.com/acme/webapp/pull/505

gh issue develop creates a branch and links it to issue #482 immediately – the link is visible in the issue’s Development sidebar before a single commit exists. The closing keyword in the pull request body is still what makes the issue actually close on merge; the linked branch alone only provides a visible “work is in progress” association.

How it works step by step

  1. You write a closing keyword and issue number into a commit message, a pull request title, or a pull request description, either directly or as plain prose – not inside a fenced code block or inline code span.
  2. On every push and every edit to the pull request text, GitHub re-parses the combined text (commit messages plus the current title and description) for the keyword-plus-number pattern.
  3. A match creates a cross-reference timeline event on both items, and if a recognized closing keyword was used, the pull request is marked internally as “will close #482 when merged” – visible as a linked issue in the sidebar.
  4. When the pull request is merged (via merge commit, squash, or rebase merge) into the repository’s default branch, GitHub re-evaluates the recorded closing references and, for each one, calls its API to close the issue and post a “closed this via #501” event on the issue’s timeline.
  5. If the pull request is closed without merging, or merged into a branch other than the default branch, no issue closes – the cross-reference simply remains as an open link.
  6. Editing a pull request’s description after it has already been merged does not retroactively close anything; closing only happens at merge time.

Common Mistakes

Mistake: relying on a keyword that only lives in one commit that later gets reworded.

git commit -m "wip: session timeout logic"
git commit -m "Closes #482"
git commit -m "cleanup: remove console.log"

If a squash merge’s final commit message is retyped by a reviewer in the GitHub UI and the Closes #482 line gets dropped, the issue never closes even though the code shipped. The fix is to also put the closing keyword directly in the pull request description, since GitHub checks the description independently of whatever text the final merged commit ends up with.

Mistake: using a bare issue number when you mean a different repository.

# Wrong: bare number resolves to an issue in *this* repo, not docs-site
git commit -m "fix: correct typo pulled from upstream

Fixes #77"

# Right: explicit owner/repo makes the cross-repo target unambiguous
git commit -m "fix: correct typo pulled from upstream

Fixes octo-org/docs-site#77"

Without the owner/repo prefix, #77 silently links to whatever issue or pull request happens to be numbered 77 in the current repository – which is almost certainly the wrong thing, and easy to miss during review because the link still looks valid.

Mistake: adding a closing keyword to a pull request whose base branch is not the default branch. A keyword only fires when the merge target is the repository’s default branch. If the pull request’s base is a long-lived integration branch, merging it will not close the issue – it stays open until that integration branch is itself merged into main. Retarget the base branch, or accept the delayed close.

Mistake: writing the keyword inside a fenced code block. GitHub’s parser ignores text inside triple-backtick fences and inline code spans, so a closing keyword shown as part of an example command in the description (say, while documenting how you tested the fix) will not register. Keep the actual closing reference in plain prose, outside any code formatting.

Best Practices

  • Put the closing keyword in the pull request description, not only in a commit message – squash and rebase merges can alter or drop individual commit text.
  • Use gh issue develop <number> --checkout or the issue’s “Create a branch” button to generate a branch that is pre-linked to the issue.
  • Name branches after the issue number for human readability (e.g. 482-session-timeout), but do not rely on the branch name for GitHub’s linking – only keyword text and manual links count.
  • Prefer one focused issue per pull request when practical; a PR that silently closes three unrelated issues makes release notes harder to read.
  • Always use the explicit owner/repo#number form for cross-repository references so the target is unambiguous.
  • When a keyword does not fit the sentence naturally, use the manual Link a pull request action from the issue’s Development sidebar instead of forcing awkward prose.
  • Check the pull request’s Development sidebar before merging – it lists exactly which issues will close, catching a missing or wrong reference while it is still cheap to fix.

Practice Exercises

  1. Create an issue in a repository you own. Create a branch, commit a small change, and open a pull request whose description includes a closing keyword referencing that issue. Confirm the Development sidebar shows the issue as linked to close, then merge and verify it closed automatically.
  2. Using two repositories you have write access to (or a fork of a public repo), open a pull request in repository A that includes Fixes ownerB/repoB#N for a real issue in repository B. Merge it and check whether the issue in repository B actually closed.
  3. Open an existing pull request that has no closing keyword anywhere in its text. Using only the issue’s Development sidebar – without editing any commit or pull request text – manually link it to a related issue, and confirm the link appears on both sides without closing the issue.

Summary

  • Issues and pull requests share one number sequence per repository; GitHub links them by scanning commit messages, PR titles, and PR descriptions for issue numbers.
  • A bare #123 only cross-references; a closing keyword (close/fix/resolve and their variants) placed right before the number schedules the issue to close on merge.
  • Auto-closing only happens when the pull request merges into the repository’s default branch, never on merge to another branch, and never retroactively.
  • Use owner/repo#123 to link across repositories; without the prefix, the number always resolves to the current repo, and closing a cross-repo issue requires permission in that repo.
  • Keywords inside fenced code blocks or inline code are ignored by GitHub’s parser.
  • An issue and a pull request can always be linked manually from the Development sidebar when a keyword does not apply.
  • gh issue develop creates a branch that is automatically linked to an issue from the moment it exists.