Skip to content

Create a git worktree from a GitHub issue

worktree can derive branch names directly from GitHub issues, and assign the issue to you at the same time. The issue is read from your repository, or from the one it was forked from; see Forks.

Terminal window
worktree branch --github 42
worktree branch --github "#42"
worktree branch --github https://github.com/acme/widgets/issues/42
worktree branch --github 42 --assign

The generated branch name is shown as the default in the prompt, so you can accept it or edit it before creation. A non-interactive run takes it without asking.

A pasted URL must be an issue (or pull request) URL of the repository chosen as described under Forks, compared ignoring case. A URL for another repository exits 2 with invalid_value, naming both and github.repository, before anything is created; set github.repository to that repository, or pass the number. The URL’s host must be github.host (github.com, or www.github.com, when it is unset), compared ignoring case; a URL on any other host exits 2 with invalid_value before anything is created, and the URL’s host is never contacted.

--github and --assign read and assign in one repository, chosen in this order:

  1. The github.repository key, an owner/repo such as acme/widgets. It is read from git config, then from .worktree.yml.
  2. The upstream remote, when it exists and is on github.host (github.com unless you set one).
  3. The origin remote.

With no upstream remote and no key, that is origin, as it has always been, and the output is unchanged. In a fork, origin is your copy and the issues usually live on upstream, so --github 42 reads issue 42 of the original with no setup. An upstream that is not on github.host is skipped, not an error.

Whenever the repository is not the one behind origin, the output says which one it used and why. The spinner reads Fetching GitHub issue #42 from acme/widgets (upstream), the assignment line ends in acme/widgets (upstream), and a failure names the repository in the same form. The part in brackets is upstream or github.repository. On a GitHub Enterprise host the repository is written <host>/<owner>/<repo>, for example ghe.example.com/acme/widgets (upstream); on github.com the text is unchanged.

If your issues are on the fork, or on a repository neither remote points at, pin it:

Terminal window
worktree config github.repository <owner/repo>

github.repository is a team key: commit it under github: in .worktree.yml to say it once for everyone, and any teammate can override it from git config. Only owner/repo is accepted; a URL, a path or a trailing .git is refused. worktree config never asks for it, since most people have no fork, so set it by name.

A token that can read your fork may not be allowed on upstream; a private upstream answers 404, and the error names the repository it asked.

--github and --assign work against GitHub Enterprise Server and GitHub Enterprise Cloud with data residency (*.ghe.com), one host at a time. The host is configured, not guessed from a remote, and the clone has to agree with it.

Terminal window
worktree config github.host ghe.example.com

That sets it for this repository, in git config. To say it once for everyone, commit it under github: in .worktree.yml, where a teammate can still override it from git config:

github:
host: ghe.example.com

The value is a bare hostname, such as ghe.example.com: no https://, port, path or trailing dot. worktree config never asks for it, since most people use github.com, so set it by name. github.com is accepted and means the default, so a repository can switch back from a team-wide Enterprise value. Any other host under github.com, such as api.github.com, is refused.

The host has to be named twice, by github.host and by a remote of this clone. Requests go to the configured host only when the remote the repository was read from is on that host. When github.repository supplies the repository, origin has to be on the host instead. A merged .worktree.yml change therefore cannot send your token to a new host on its own.

  • An upstream on another host is skipped, as in Forks.
  • An origin on another host is an error that names the remote, the host expected and github.host. Correct github.host, or unset it to use github.com.
  • On github.com, a non-GitHub origin keeps its usual error and adds how to set an Enterprise host.

Every common remote form is read: https://, ssh:// (and its older spellings git+ssh:// and ssh+git://), git:// and the scp-like git@host:owner/repo.git. The host is compared ignoring case, and a port is ignored, since an SSH port is not the API’s port. ssh.github.com and www.github.com count as github.com.

The token comes from the GitHub CLI, for that host only:

Terminal window
gh auth login --hostname ghe.example.com

For a run without a stored gh login, such as CI, set GH_ENTERPRISE_TOKEN (Server hosts); worktree does not read it itself, gh auth token --hostname <host> returns it. See the gh environment reference. The stored github.token is never used for an Enterprise host. See Authentication Resolution.

A host ending in .ghe.com uses https://api.<host>. Any other host uses https://<host>/api/v3. The scheme is always https, whatever scheme your remote uses, so a token never travels in clear. A repository on an Enterprise host is written <host>/<owner>/<repo> in the output.

Enterprise Server often has a certificate from your company’s own authority, which Node does not trust. The run then fails with GitHub: could not reach <host>. and the TLS reason. Point Node at the authority’s certificate (a PEM file) before running worktree:

Terminal window
export NODE_EXTRA_CA_CERTS=/path/to/company-ca.pem

or, on Node.js 22.19 or 24.6 and later, let Node trust the operating system’s certificate store:

Terminal window
export NODE_USE_SYSTEM_CA=1

Both are read when Node starts, so set them in the shell before the command. See the Node.js command-line reference.

  1. Read issue number and title from the GitHub API.
  2. Sanitize the title into a branch-safe slug: lowercase letters, digits and dashes only, so a title can never add a / or .. to the name.
  3. Cut the slug to at most 48 characters at the last dash, so it ends on a whole word. A single word longer than 48 characters is cut at 48.
  4. Optionally prepend a configured prefix based on issue type, giving <prefix><number>-<slug> by default; a branchName template in .worktree.yml changes the shape (see Branch name template).

For example, an issue 42 titled “Add a dark mode toggle to the settings page so that users can switch themes at runtime” becomes 42-add-a-dark-mode-toggle-to-the-settings-page-so. The name is the same whether or not you are prompted.

Supported issue-type prefix mapping:

  • Feature -> branchPrefix.feature
  • Bug -> branchPrefix.bugfix
  • Task -> branchPrefix.chore

If a prefix key is not configured, no prefix is applied.

An issue with no type, or with a type other than these three, gets no prefix, whatever its labels say. That default stays as it is so that branch names people already get do not change. To choose the prefix yourself, add a github.branchType list to .worktree.yml, or a shared top-level branchType list. It can match the issue’s Type and its Labels (by name or by { id: ... }), and ending it with - otherwise: feature gives the issues no rule matches a prefix:

github:
branchType:
- when: { Type: Bug }
type: bugfix
- when: { Labels: [chore, maintenance] }
type: chore
- otherwise: feature

To write the rules once for every provider you use, put the same list at the top level instead (branchType: rather than github: and branchType:). A github.branchType list still wins over it for GitHub issues, and either way the built-in mapping above is not used. See Branch-type rules for how rules match, and Branch name template to change the shape of the name.

On github.com, authentication is resolved in this order:

  1. Existing github.token in local git config.
  2. gh auth token --hostname github.com from GitHub CLI. The token is read on each run and is never saved to git config, so it cannot go stale or sit in clear text. Because the host is named, neither GH_HOST nor a gh login to another host supplies it.
  3. Interactive prompt for a token, then save to local git config. A non-interactive run has no prompt: it exits 2 naming worktree config github.token <token> or gh auth login, and never prints a token.

Only a token you type at the prompt or set with worktree config github.token <token> is stored. If GitHub refuses a token with 401, the error says so. A stored token, including one you have just typed at the prompt, wins over gh, so when it is the one refused, running gh auth login does not help: the error tells you to replace it with worktree config github.token <token>, or to remove it so gh auth token is used again:

Terminal window
git config --unset northguild.worktree.github.token

A token saved by an earlier version stays stored until you run that command or replace it.

On github.com, a token that came from gh gets a shorter hint: gh auth login, or worktree config github.token <token>.

On a GitHub Enterprise host (github.host) there is one source only: gh auth token --hostname <host>. The stored github.token is never read for that host and never sent to it, and nothing is prompted for, since a typed token would be stored with no host. With no token the run exits 2 with missing_value, naming gh auth login --hostname <host>; a 401 names the same command. See GitHub Enterprise.

Reading an issue and assigning one need different amounts of access. A public repository answers an unauthenticated read, which is why --github has never required a token there. Assigning always does, and the token has to be allowed to write: Assigning needs a token whose account has push access to the repository; a fine-grained token also needs the Issues (or Pull requests) permission set to Read and write. So the first time you assign on such a repository you may be asked for one, unless the resolution order above already supplies it.

Creating the branch is the moment you have demonstrably started work, so worktree can say so on the issue for you.

The decision is made in this order:

  1. --assign assigns it to the authenticated user; --no-assign skips it for that run.
  2. github.autoAssign: true always, false never. An explicit false is respected in every mode.
  3. A non-interactive run assigns. The default is not saved, so one unattended run does not settle the key.
  4. Otherwise you are asked once, and your answer is saved to github.autoAssign.

To change your mind later:

Terminal window
worktree config github.autoAssign true

worktree confirms the assignment against the API response rather than assuming it worked, and reports a warning if the issue comes back without you on it. A failed assignment never stops the branch — the worktree is created regardless.

Terminal window
worktree config github.token <token>
worktree config github.autoAssign true
worktree config branchPrefix.feature feature/
worktree config branchPrefix.bugfix fix/
worktree config branchPrefix.chore chore/

You can also configure only missing values:

Terminal window
worktree config --missing --names github.token,github.autoAssign,branchPrefix.feature,branchPrefix.bugfix,branchPrefix.chore