Skip to Content

worktree branch

Create a new branch in a dedicated worktree.

Usage

worktree branch <name> worktree branch <name> --source <remote-branch> worktree branch --github <issue-number> worktree branch --github <issue-number> --assign worktree branch --github <issue-number> --no-assign worktree branch --jira <issue-id> worktree branch <name> --agent "<prompt>" worktree branch <name> --agent-file <path> worktree branch <name> --agent-stdin worktree branch <name> --no-agent worktree branch <name> --no-open worktree branch <name> --install worktree branch <name> --no-install worktree branch <name> --json

Examples

worktree branch feature/add-bulk-actions worktree branch feature/add-bulk-actions --source origin/release/1.4 worktree branch --github 42 worktree branch --github "#42" worktree branch --github 42 --assign worktree branch --github 42 --no-assign worktree branch --jira DEV-123 worktree branch --jira dev-123 worktree branch feature/add-bulk-actions --agent "add bulk actions to the table" worktree branch --github 42 --agent "implement the issue" worktree branch --github 42 --agent-file brief.md cat brief.md | worktree branch --github 42 --agent-stdin worktree branch --github 42 --no-agent worktree branch feature/add-bulk-actions --no-open worktree branch feature/add-bulk-actions --install worktree branch feature/add-bulk-actions --no-install worktree branch --github 42 --json --agent-file brief.md

GitHub Issue Branching

Use the --github flag to create a suggested branch name from an issue in the current repository.

  • The issue number can be passed as 42 or "#42".
  • The generated name is pre-filled in the prompt and still editable. A non-interactive run takes it as it is.
  • If issue types are configured in the repository, these optional prefixes are applied:
    • branchPrefix.feature for issue type Feature
    • branchPrefix.bugfix for issue type Bug
    • branchPrefix.chore for issue type Task

If no prefix is configured, worktree falls back to an empty prefix and still generates a valid branch name.

The name is <prefix><number>-<slug>. The title becomes a lowercase slug of letters, digits and dashes, cut to at most 48 characters at the last dash, so the name ends on a whole word. A single word longer than 48 characters is cut at 48. The name is the same whether or not you are asked, so one issue never gives two different branches.

GitHub authentication is resolved in this order:

  1. northguild.worktree.github.token from local git config.
  2. gh auth token if GitHub CLI is installed and authenticated.
  3. Interactive token prompt, which is then saved in local git config.

A non-interactive run has no token prompt. It exits 2 with one line on stderr, which never includes a token:

worktree: no default for a GitHub token; pass `worktree config github.token <token>` or run `gh auth login`

Assigning the issue to yourself

--assign assigns the issue to the authenticated user as part of creating the branch, and --no-assign skips it for that one run. Without either flag the decision is made in this order:

  1. --assign or --no-assign.
  2. github.autoAssign: true assigns, false never does.
  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 so the question is not repeated.

A configured false is respected in every mode; pass --assign to override it for one run.

worktree branch --github 42 --assign worktree branch --github 42 --no-assign worktree config github.autoAssign true # always assign, stop asking
  • Assigning requires a token with push access to the repository. Reading an issue does not, so a public repository that --github has always worked against without a token may ask for one the first time you assign — the resolution order above still applies, so an authenticated gh supplies it silently.
  • Success is confirmed against the API response rather than assumed. If the issue comes back without you on it, worktree says so instead of reporting success — GitHub can accept an assignee change and ignore it when the token lacks push access.
  • A failed assignment never stops the branch. The worktree is created either way; you get a warning, not an error.
  • The assignment happens before the worktree is created, so the question is never buried behind creation output or a launched editor. Assigning is idempotent — GitHub does not replace existing assignees — so retrying after a failed creation is harmless.
  • --assign and --no-assign both require --github or --jira; on their own there is no issue to act on. --assign --jira warns that Jira assignment is not supported yet and carries on.

Installing dependencies

After the worktree exists and its env files are copied, branch can install its dependencies, before anything is opened or any agent is started, so neither lands in a tree without them.

  • The command is postCreate when it is set. Otherwise it is inferred from the first lockfile found in the new worktree:

    LockfileCommand
    pnpm-lock.yamlpnpm install --frozen-lockfile
    package-lock.jsonnpm ci
    yarn.lockyarn install
    bun.lockbun install --frozen-lockfile

    bun.lockb is not recognised. yarn install has no lock flag because classic and berry disagree on it.

  • When it runs: a non-interactive run installs by default. A terminal run installs only when postCreate is set or --install is given. --no-install switches it off for the run, whatever else is set.

  • How it runs: by argv in the new worktree, never through a shell, with its output on stderr and a 10 minute limit.

  • Nothing to run: with no postCreate and no lockfile, or when the program is not on PATH, branch says why on stderr and carries on.

  • A failed install (non-zero exit, or the limit reached) keeps the worktree, opens nothing, starts no agent, and exits 1. Nothing is rolled back. Fix the install in the worktree and run worktree open <branch>, or run worktree remove <branch> -f and re-run branch with --no-install. Re-running branch as it was fails, because the branch already exists.

checkout has no install step.

Jira Issue Branching

Use the --jira flag to create a suggested branch name from an issue in your Jira workspace.

  • The issue ID can be passed in any case (dev-123, DEV-123, etc.).
  • The generated name is pre-filled in the prompt and still editable.
  • If issue types are configured in the repository, these optional prefixes are applied:
    • branchPrefix.feature for issue types like Feature, Story, and most non-bug/chore types
    • branchPrefix.bugfix for issue type Bug
    • branchPrefix.chore for issue type Task

If no prefix is configured, worktree falls back to an empty prefix and still generates a valid branch name.

Jira authentication requires three configuration keys:

  1. northguild.worktree.jira.host - Your Jira workspace URL (e.g., https://company.atlassian.net)
  2. northguild.worktree.jira.email - Your Jira email address
  3. northguild.worktree.jira.apiToken - Your Jira API token

Agent Dispatch

Use the --agent flag (short: -a) to hand the new worktree to a coding agent with a prompt, called the brief. The brief can come from one of three places, and they are mutually exclusive:

FlagThe brief is
--agent "<text>"the flag’s value
--agent-file <path>the whole of a file, which has to exist and be a regular file
--agent-stdinthe whole of piped stdin; a terminal on stdin is refused rather than waited on
worktree branch feature/add-bulk-actions --agent "add bulk actions to the table" worktree branch --github 42 --agent-file brief.md

An empty brief, or one over 131,071 bytes (the most a single argument can carry on Linux), is rejected before anything is created. The brief is delivered verbatim, so quotes, apostrophes, semicolons and newlines in it reach the agent unchanged and no shell interprets it.

There is one agent per worktree, started one of two ways:

  • When the worktree opens as a Herdr space (opener herdr), the agent is started in the space’s pane with herdr agent start, and the brief is then submitted to it with herdr agent prompt, once, after the agent is ready. The brief is never part of the start command.
    • The agent kind is herdr.agent, or else the program agent.command names. With a brief and neither, and Herdr as the opener and installed, the run exits 2 before the worktree is created, with worktree: no default for the agent kind; pass `worktree config herdr.agent <kind>`.
    • When agent.command names the same program, its arguments are passed to the agent, minus --bg and --background (a note says so on stderr): Herdr runs the agent in the pane, so a background flag would take it out of sight.
    • For claude, --name <session> is added. The session name is <repo>-<branch>, lowercased, with every run of characters outside a-z, 0-9 and - turned into -, and never shortened. It is printed on stderr. Herdr’s own handle for the agent is separate and capped at 32 characters.
    • If starting the agent fails, or the brief cannot be delivered, a warning says so (a failed delivery names the brief by its size, never its text), the worktree and space are kept, and the run exits 1.
  • Otherwise (editor opener, opener none, or Herdr failing to open) agent.command is launched detached with the brief as its last argument and the new worktree as its working directory. It is started and left running; worktree does not wait for it and its output does not appear here. With a brief, no agent.command and a run Herdr will not open (editor, none, --no-open, or herdr not on your PATH), the run exits 2 before the worktree is created, with worktree: no default for the agent command; pass `worktree config agent.command "<command>"`. If Herdr was expected to open and failed, and there is no agent.command to fall back to, the worktree is kept and the run exits 1.

--no-agent opens the worktree as usual but starts no agent. --no-open creates the worktree and prints Worktree created at <path> without calling Herdr or the editor, for this run only, like opener none; an agent is still started, detached, when a brief is given, unless --no-agent says otherwise.

checkout takes --agent and follows the same rules.

Non-interactive runs

A run is non-interactive when stdin is not a terminal, CI is set (and is not empty, 0 or false), --non-interactive or --yes (-y) is given, or --json is given; every command accepts the two flags. It never prompts and never animates; progress goes to stderr as plain lines.

  • A prompt with a default takes it: the issue-derived branch name. An unset github.autoAssign assigns the issue (without saving the key); false or --no-assign still skips it.
  • A prompt with none fails at once, exit 2, with one line on stderr: worktree: no default for <value>; pass <flag>. Here that is the branch name (<branchName>), and a --source without the origin/ prefix (--source origin/<branch>).
  • Dependencies are installed by default, from postCreate or the lockfile; --no-install skips it.
  • Missing config is not offered to config: defaultSourceBranch unset warns and falls back to origin/main.

What It Does

  1. Assigns the issue to you, when creating from --github and assignment is enabled.
  2. Creates a branch from your configured source branch.
  3. Adds a worktree under <repo>.worktrees/<branch-name>.
  4. Copies the gitignored env files into the new worktree — see Environment Files.
  5. Installs dependencies in it, when enabled. A failure stops here with exit 1.
  6. Opens the worktree in your configured editor (if configured), or as a Herdr space when opener is herdr; with opener none or --no-open, opens nothing and prints Worktree created at <path>.
  7. Starts the coding agent and delivers the brief (with --agent, --agent-file or --agent-stdin), unless --no-agent — see Agent Dispatch.

JSON output

--json makes stdout carry exactly one JSON document and nothing else, on one line, for a script or another agent to parse. It implies a non-interactive run. Everything meant for a person goes to stderr instead: the spinner lines, the env-file list, notes and warnings.

{"path":"/abs/repo.worktrees/70-headless-agent-mode","branch":"70-headless-agent-mode","source":"origin/main", "issue":{"provider":"github","number":70,"url":"https://github.com/acme/demo/issues/70"},"assigned":true, "envFilesCopied":["docs/.env.local"], "installed":{"ran":true,"command":"pnpm install --frozen-lockfile","inferred":true,"ok":true}, "herdr":{"space":"w5","pane":"w5:p1","agent":"wt-70-headless-agent-mode"}, "agent":{"name":"demo-70-headless-agent-mode","kind":"claude","command":["claude","--name","demo-70-headless-agent-mode"],"prompted":true}, "warnings":[]}
KeyValue
path, branch, sourcethe worktree’s absolute path, its branch, and the ref it was created from
issue{provider:"github", number, url} or {provider:"jira", key, url}; null when no issue was used. url is null when it is not known
assignedtrue or false once an assignment was attempted; null when none was (no issue, --no-assign, or a configured false)
envFilesCopiedthe env files copied, relative to the repository root; [] when there were none
installed{ran:true, command, inferred, ok}, plus reason when ok is false; {ran:false, reason} when the step was skipped
herdr{space, pane, agent} for the Herdr space that was opened; null when Herdr was not opened. agent is Herdr’s handle, null when no agent was started in the pane
agent{name, kind, command, prompted} for the agent that was started; null when none was. command leaves out the brief. name is the session name, null where worktree named nothing (a detached start)
warningswhat went wrong without stopping the run: a failed assignment, a Herdr or agent failure, missing config

A failed install still prints the document, with installed.ok false and the message in warnings, and exits 1: the worktree exists, so its path is reported rather than left out of an error. A brief that was given and reached no agent is reported the same way, with agent null or agent.prompted false — agent.prompted is the receipt that the agent has the brief.

A failure otherwise prints a different document, still one line on stdout, and exits non-zero. A line describing it goes to stderr as usual:

{"error":{"code":"missing_value","message":"no default for the branch name; pass <branchName>","details":{"value":"the branch name","flag":"<branchName>"}}}
codeMeansExit
missing_valuea value with no default was not given; details names it and the flag2
invalid_valuea value or flag combination was refused: a bad branch name, an empty brief, a malformed --github or --jira issue id2
not_foundsomething named does not exist, such as the --source branch2
timeouta call that is bounded did not answer in time1
failedanything else1

No token or password is ever printed: URL credentials and token-shaped strings are masked in every message.

Last updated on