Repository navigation
Conversation
Sessions in one case share its folder, so several agents on one repository edit, build and commit in the same checkout and step on each other. A case whose folder is a git repository can now keep worktrees under <repo>/.claude/worktrees/<name> on a new branch (default worktree-<name>), which is Claude Code's own --worktree layout (measured in Claude Code 2.1.296) and the one Codeman's worktree pill already recognizes. New module src/git-worktrees.ts (pure name/branch gates, worktree list parsing, exclude text; IO through an injectable runner) and routes GET/POST /api/cases/:name/worktrees and DELETE /api/cases/:name/worktrees/:worktree: - git runs as argv with -- before paths, non-interactive env, process group timeouts, --no-optional-locks on reads; names are gated before git and confirmed with git check-ref-format --branch. - Only <repo>/.claude/worktrees/<valid-name> is created or removed; every path component is lstat'ed and realpath-checked, so a symlinked .claude cannot redirect a create or remove outside the repository. - /.claude/worktrees/ (and the per-workspace hooks file Codeman writes, which otherwise blocks git worktree remove) go to the repository's local info/exclude, never a tracked .gitignore. - Removal is git worktree remove WITHOUT --force, refused while a live session runs inside; branch delete is git branch -d, never -D. - Docker and remote cases are refused with a 400, and git never runs for a case at or inside a Docker case workspace. Starting a session reuses the existing create paths: quick-start takes a worktree name (validated, local cases only) and runs hooks, the skill, the model override and the session itself in the worktree; POST /api/sessions takes the worktree as workingDir. The unified session list names the worktree of a live session from its folder layout, so a codex or shell session in one gets the worktree pill too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The worktree API needs a front door that works from the place cases are already picked. The toolbar case picker gains a third action row, "Git worktrees…" (the phone case sheet gets a Worktrees button next to Create New Case; no header button anywhere), opening one dialog per case: - the repository's worktrees, with the managed ones (.claude/worktrees) offering Open (start another session in it) and Remove…, an inline confirmation with "Also delete its branch" (git branch -d) whose git refusal is shown verbatim through textContent; - a New worktree form (name, optional branch and start point) and a "Start sessions with" picker over every enabled, installed run mode, built from the registry catalogue as data, never an id branch. Launching goes through run() with options.worktree and the same temporary _runMode swap the custom-model entries use, so every mode keeps its own path: claude and shell create through POST /api/sessions with the worktree as workingDir, every other CLI through quick-start's worktree field. Tabs are named w<n>-<worktree> and the Run button's mode is left as it was. Close Session offers "Kill & remove worktree" for a session that runs in a Codeman-managed worktree; the removal waits for the server to confirm the session is gone (closeSession now resolves true/false), and a dirty tree keeps the worktree with git's reason in a toast that opens the dialog. zh-CN strings and toast patterns for all of it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Documents the worktree endpoints and quick-start's worktree field in docs/api-reference.md, adds a Git Worktrees page to the wiki (sidebar, Home index, a pointer from Core Concepts where sessions sharing one case folder is explained), and records the rules that keep the feature safe in docs/architecture-invariants.md with a short CLAUDE.md pointer: only the managed .claude/worktrees path is ever touched, git never runs over a Docker workspace, removal is never forced and branch delete is -d only, and sessions start through the existing create paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…emoval The removal guard and the per-worktree session count compared each session's workingDir lexically against the worktree paths git lists, which are real paths. POST /api/sessions stores workingDir as it was spelled, so a session started in a worktree of a linked case whose folder is a symlink (a repository on another disk, macOS /tmp, a symlinked cases folder), named through that link, counted for no worktree, and DELETE /api/cases/:name/worktrees/:worktree removed the folder under the running agent (reproduced: 200 instead of 409). Each session folder under the case path is now respelled onto the repository's real top level before the comparison (respellUnder, pure and lexical, so no filesystem call per session). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a managed worktree's folder is deleted by hand (a user in a file manager, an agent's rm -rf), git keeps its record and lists it as prunable. Codeman then had no way out: the dialog showed the row as "missing" with no action, DELETE answered "No worktree named ...", and the name could never be used again (the branch still existed, and with another branch git refused "a missing but already registered worktree"). Reproduced on git 2.43; git's own `git worktree remove` on the missing path drops the record with exit 0. Removal (only removal, never a session start) now accepts a worktree whose folder is gone when git lists it at exactly the managed path, as prunable or locked, and no component on the way is a symlink or a file. The live-session and lock refusals still apply. The dialog offers Remove... (not Open) on such a row, with a note saying what it does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Codeman Agent Skill on (agentSkillEnabled), every session create writes the skill into the folder the session runs in, so a session in a worktree leaves an untracked .claude/skills/codeman/ (claude) or .agents/skills/codeman/ (codex, gemini and the rest) behind. git then refuses `git worktree remove` for that worktree forever, and an agent running `git add -A` there would commit Codeman's skill into its branch (reproduced: the worktree's status showed "?? .agents/" and "?? .claude/"). A create now excludes <skills dir>/codeman/ next to the hooks file, for every skills folder the registry names (capabilities.agentSkillDir, read at call time by the route and passed in as data, so no CLI id is branched on). Like the other lines it is only added when git does not already ignore the path, and only to the repository's local info/exclude. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Codeman runs several agent sessions per case, and every session in a case shares the case folder. For a git repository that is a problem as soon as two agents work at once: they edit the same files, overwrite each other's builds and commit each other's half-finished work. This PR lets Codeman create, open and remove git worktrees for a local case whose folder is a repository, so each agent gets its own checkout on its own branch, and start sessions inside them through the existing launch paths. It does not touch Docker or remote (SSH) cases (both are refused with a 400 before any git runs), the tracked
.gitignoreof a repository, or how sessions outside a worktree are created.How it works
<repo>/.claude/worktrees/<name>on a new branchworktree-<name>. That is the layout Claude Code's own--worktreeflag uses (measured in 2.1.296), and Codeman's name rules are a strict subset of its rules, so either tool recognizes the other's worktrees.src/git-worktrees.tshas a pure half (name, branch and base gates,git worktree list --porcelainparsing, exclude text) and an IO half that runs git through an injectable runner: argv only,--before path operands, the non-interactive environment, a timeout that kills the whole process group. Routes insrc/web/routes/worktree-routes.ts:GET /api/cases/:name/worktreeslists the case repository's worktrees, which ones Codeman manages, and the live sessions in each.POST /api/cases/:name/worktreeswith{ name, branch?, base? }creates one. Only a new branch (-b, never-B), from a base resolved to a commit first.DELETE /api/cases/:name/worktrees/:worktree[?deleteBranch=true]runsgit worktree removewithout--force(git's refusal on a dirty tree is the answer), refused while a live session runs inside; the branch delete isgit branch -d, never-D.<repo>/.claude/worktrees/<valid-name>is ever created or removed. Every component islstated and realpath-checked, so a symlinked.claudecannot redirect a create or remove. git never runs at or inside a Docker case workspace, sincegit worktree addruns the repository's hooks and checkout filters on the host./.claude/worktrees/is appended to the repository's local.git/info/exclude(never a tracked file) when git does not ignore it already, together with the files Codeman itself writes into every workspace: the hooks file.claude/settings.local.jsonand the agent skill copy (<skills dir>/codeman/). Without those lines every worktree a session ran in looked modified, andgit worktree removerefused it.POST /api/quick-starttakesworktree: "<name>";POST /api/sessionstakes the worktree path asworkingDir. The UI launches through the Run button's ownrun()with a temporary run-mode swap, and tabs are namedw<n>-<worktree>.This branch also carries three fixes from review, each with a regression test:
workingDirthey were created with, while git lists real paths, so a session in a worktree of a linked case whose folder is a symlink counted for no worktree, and DELETE removed the folder under the running agent (reproduced: 200 instead of 409). Session folders under the case path are now respelled onto the repository's real root before comparing (lexical, no filesystem call per session).Testing
Unit and route tests (all against real git where it matters):
test/git-worktrees.test.ts: pure gates, then a real repository: create, exclude lines, symlink guards, dirty/live/locked refusals,branch -d, missing worktree removal (folder gone, locked, symlink on the way).test/routes/worktree-routes.test.ts: a real repository through the routes and quick-start; Docker refusal with a runner that counts calls (zero); remote, non-repository, missing case, bad names, symlinked.claude, dirty tree, live session, multi-user ownership; plus the three review fixes (symlink-spelled session, missing folder, agent skill copies).test/worktree-ui.test.ts(shipped session-ui.js in a vm and JSDOM),test/worktree-modal-i18n.test.ts(real translator over the real index.html),test/session-worktree-label.test.ts.npm run typecheck,npm run lint,npm run format:check,npm run check:frontend-syntax(46 files),npm run check:public-assets(63 files) all clean; fullnpm test: 571 files passed, 1 skipped; 11,349 tests passed, 12 skipped, 0 failed.Live end-to-end on an isolated instance (
CODEMAN_INSTANCE=wtrev, port 5334, scratch repository with one commit; boot showed 0 sessions and no restore lines):api-wt201 (excludeAdded:/.claude/worktrees/,/.claude/settings.local.json,/.claude/skills/codeman/,/.agents/skills/codeman/);../escape400; same name again 409; a non-repository case 400; unknown case 404.git worktree listshowed[main]and[worktree-api-wt]; maingit status --porcelainempty.worktree: "api-wt": in the pane,pwd; git branch --show-currentprinted.../wtrepo/.claude/worktrees/api-wtandworktree-api-wt. DELETE while it lived: 409 "A live session is still running in worktree "api-wt"". The unified session list row carriedworktreeName: "api-wt".wtrepo-link), a worktreesym-wtcreated through it, a session created withworkingDirspelled through the link. DELETE: 409, the list showedsessionCount: 1, folder still there.["New or link a case…","Case settings…","Git worktrees…"];../badshowed the inline error; "ui-wt" with Claude Code then "Create & start session" opened tabw1-ui-wtwithworkingDirin the worktree; worktree and main status both empty with Claude's hooks file present.gone-wtcreated, its folder deleted; the row showed "missing" with only Remove… and the note "Its folder is gone. ..."; Remove with the branch option: "Worktree gone-wt and branch worktree-gone-wt removed"; creatinggone-wtagain: 201.skill-wtwith skill copies in.claude/skills/codeman/and.agents/skills/codeman/: worktree status empty, DELETE 200 withbranchDeleted: true.Not run:
npm run test:browser,test:mobile,test:perf(the Playwright run above covered the UI paths).Reviewing the diff
About 3,400 added lines, but most of it is mechanical:
test/git-worktrees.test.ts,test/routes/worktree-routes.test.ts,test/worktree-ui.test.ts,test/worktree-modal-i18n.test.ts, small edits to two existing tests).docs/api-reference.md,docs/architecture-invariants.md, the new wiki pagedocs/wiki/Git-Worktrees.mdand its index entries, a shortCLAUDE.mdpointer).index.html(63), route registration (3).The roughly 1,250 lines that need real review, in this order:
src/git-worktrees.ts(937 lines, about 650 of code): read the@fileoverviewrules first, thenresolveManagedWorktree,createCaseWorktree,removeCaseWorktreeandensureExcluded. This is the security-relevant part (paths, symlinks, argv).src/web/routes/worktree-routes.ts(about 220 lines of code): case resolution mirrors quick-start (remote, Docker, linked admin-only in multi-user mode, own case space, bounded probe), and the live-session check.src/web/routes/session-routes.ts(about 25 lines of logic; the rest isresolvedCasePathrenamed toworkspacePathwhere the session's folder is meant).src/web/public/session-ui.js: the dialog and_launchInWorktree(about 400 lines); therunClaude/runShellhunks are mostly the old case lookup re-indented into anelse. ThencloseSession/confirmCloseSessioninapp.js(about 30 lines).Risk
.git/info/excludeof the user's repository. Besides the worktrees folder, Codeman now hides.claude/settings.local.jsonand its agent skill copy fromgit statusin the main checkout too, since one exclude file serves the main checkout and every worktree. Tracked files are unaffected, but a user who wants to commit one of those untracked files needsgit add -f. The alternative (leaving them visible) makes every worktree permanently unremovable without--force, which Codeman never uses..claude/worktrees/<name>as managed, including worktrees Claude Code created itself. They get Open and Remove in the dialog. Removal is still guarded by git (dirty or locked trees are refused, and Claude Code locks the worktrees its live agents use) and by the live-session check, but a reviewer may prefer read-only rows for worktrees Codeman did not create.Follow-ups
fatal:/error:lines, so a continuation line can be lost.