docs(worktrees): API reference, wiki page and the invariants behind them

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>
This commit is contained in:
Codeman maintainer committed 2026-10-14 05:16:10 +02:00
1 parent 8cffcf4be5
commit 3952963bad
8 files changed
+123 -2

No files matched your search

+2
View File
@@ -231,6 +231,8 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
**Clone a repository as a case** (issue #236, Add Case → **Clone Repo**): `POST /api/cases/clone` clones synchronously into the caller's case space (bounded by `GIT_CLONE_TIMEOUT_MS`, no job store); `POST /api/cases/clone-preflight` checks anonymous cloneability and lists refs. Core in `src/git-clone.ts`. ⚠️ **The URL is a code-execution surface**: refuse every `::` form and a leading `-`, spawn only argv arrays with `--` before operands. ⚠️ Stay non-interactive (`gitNonInteractiveEnv()`) or the open request hangs; never collect credentials, refuse `user:password@` URLs. ⚠️ Timeout kills the process GROUP, remove the destination only if this attempt created it, and repo contents win over scaffolding (existing `CLAUDE.md` kept, hooks merged, repo `.claude/settings*` warned about). → [architecture-invariants#clone-a-repository-as-a-case](docs/architecture-invariants.md#clone-a-repository-as-a-case)
**Git worktrees of a case** (case picker → **Git worktrees…**; `src/git-worktrees.ts`, `routes/worktree-routes.ts`): a local repository case keeps worktrees at `<repo>/.claude/worktrees/<name>` on `worktree-<name>`, Claude Code's own `--worktree` layout. ⚠️ Only that managed path is ever created or removed, every component `lstat`ed and realpath-checked (a symlinked `.claude` is refused). ⚠️ git never runs for a Docker or remote case, nor at or inside a Docker workspace (`worktree add` runs hooks and filters on the host). ⚠️ Remove is `git worktree remove` WITHOUT `--force`, refused while a live session is inside; branch delete is `-d`, never `-D`. ⚠️ Sessions start through the existing create paths (quick-start `worktree`, or `POST /api/sessions` `workingDir`), never a new spawn path. → [architecture-invariants#git-worktrees-of-a-case](docs/architecture-invariants.md#git-worktrees-of-a-case)
**Cross-session search**: `GET /api/search` federates an in-memory search over session metadata, run-summary events, and attachment-history entries. The pure core `searchSources()` does substring matching with hard per-type caps: **no regex (so no ReDoS) and no filesystem reads (so no traversal)**. The server-private `externalPath` is never read. PAST sessions (#261) come from `session-history-index.ts`, a capped snapshot of the unified list filled **outside** the request path (`/api/sessions/unified` publishes it; a stale one is rebuilt fire-and-forget), that indirection is what keeps the no-fs property. ⚠️ The snapshot is stored UNSCOPED with a per-row owner and MUST be re-filtered through `canAccessOwned()` on read; history rows carry `jumpTo.kind:'resume-session'`, since a closed session has no tab to select. → [architecture-invariants#cross-session-search](docs/architecture-invariants.md#cross-session-search)
**Web tabs** (dashboard URLs as tabs): a saved URL renders as a tab beside sessions, **NOT a `SessionMode`**, and is proxied through Codeman's own origin (`/webview/<cap>/...`). ⚠️ The proxy is NOT an API surface: its capability-based auth exemption stays fenced to safe methods and non-route paths (`test/webview-auth-exemption.test.ts`). ⚠️ Iframes omit `allow-same-origin` unless `trusted`, and `Authorization`/`codeman_session` are stripped upstream in both modes. ⚠️ **Egress guard**: link-local and cloud-metadata targets are refused at save time, by a sync hostname check at each connect (IP literals skip DNS), AND on the resolved address (`webview-egress.ts`); use the `undici` package's own `fetch` + `Agent`, never Node's global fetch. ⚠️ Loopback links in agent output auto-open as proxied web tabs (`openLinkThroughWebTabIfLoopback`), but never auto-route `*.localhost` (prompt-injectable DNS). Capabilities are revoked on logout (`revokeOwner`). → [architecture-invariants#web-tabs](docs/architecture-invariants.md#web-tabs), `docs/web-tabs.md`
+36
View File
@@ -1011,6 +1011,42 @@ Both routes accept two optional query parameters, which the UI sends from its pe
Lists are capped (300 files and 50 commits per repository) while the counts stay exact. A branch with no upstream reports the commits no remote has (`HEAD --not --remotes`); a repository with no remote reports `unpushedCount: 0`, since there is nothing to push to. Concurrent polls of one folder share a single git invocation; `?fresh=1` (what the panel's Refresh button and opening the panel send) skips the short-lived caches, though it still joins a computation already running.
## Git worktrees of a case
A local case whose folder is the top of a git repository can keep **worktrees** at `<case>/.claude/worktrees/<name>`, each on its own branch, so several agents on one repository never share a checkout. This is the layout Claude Code's own `claude --worktree <name>` uses (branch `worktree-<name>`), so either tool finds the other's worktrees. Remote (SSH) and Docker cases are refused with `400 INVALID_INPUT` before git runs, and so is a local case whose folder is at or inside a Docker case workspace: `git worktree add` runs the repository's post-checkout hook and checkout filters on this machine, and a container can write there. A case folder that is not a repository, is a bare repository, or sits below the repository's top level is `400` too. The case resolves the way quick-start resolves it (a linked case is admin-only in multi-user mode, everything else is the caller's own case space; another user's case is `404`), and a case folder on a mount that does not answer is `422 OPERATION_FAILED`, never `404`.
`GET /api/cases/:name/worktrees` lists `git worktree list` for the case's repository:
```json
{
"caseName": "myrepo",
"repoRoot": "/home/me/codeman-cases/myrepo",
"worktreesDir": "/home/me/codeman-cases/myrepo/.claude/worktrees",
"truncated": false,
"worktrees": [
{ "path": "/home/me/codeman-cases/myrepo", "head": "6910cb15…", "branch": "main", "detached": false, "bare": false,
"locked": false, "prunable": false, "main": true, "isCase": true, "managed": false, "sessionCount": 0, "sessions": [] },
{ "path": "/home/me/codeman-cases/myrepo/.claude/worktrees/fix-login", "branch": "worktree-fix-login",
"main": false, "isCase": false, "managed": true, "name": "fix-login",
"sessionCount": 1, "sessions": [{ "id": "2a568379-…", "name": "w1-fix-login" }] }
]
}
```
`managed` is `true` only for `<repoRoot>/.claude/worktrees/<name>`: those are the ones Codeman starts sessions in and removes; any other worktree of the repository is listed for information. `sessionCount` counts live sessions of every owner running in that worktree (a session belongs to the deepest worktree holding its folder), `sessions` lists the ones the caller may see. `lockReason`/`prunableReason` appear when git reports them.
`POST /api/cases/:name/worktrees` with `{ name, branch?, base? }` creates one (`201`, `{ worktree: { name, path, branch, base, head, excludeAdded, warnings } }`):
- `name`: 1 to 64 characters of `A-Z a-z 0-9 . _ -`, starting with a letter or digit, no `..`, not ending in `.` or `.lock`. `400` otherwise.
- `branch`: the NEW branch, default `worktree-<name>`. Letters, digits, `.`, `_`, `-`, `/`, then `git check-ref-format --branch` must accept it unchanged. An existing branch is `409 ALREADY_EXISTS` (never reset), and so is a name whose folder already exists.
- `base`: where the branch starts, default `HEAD` (the case's current commit): a branch, tag or commit, resolved before anything is written (`400` when it is not a commit, which includes an empty repository).
- `excludeAdded` lists the lines appended to the repository's local `info/exclude` (in the real common git dir, never a tracked `.gitignore`) when git did not already ignore them: `/.claude/worktrees/`, so the main checkout does not show the worktrees as untracked, and `/.claude/settings.local.json`, the hooks file Codeman writes for every claude session, which would otherwise make `git worktree remove` refuse.
- A symlinked `.claude` or `.claude/worktrees` directory is refused (`400`): a repository cannot redirect the worktree outside itself.
`DELETE /api/cases/:name/worktrees/:worktree[?deleteBranch=true]` removes a managed worktree with `git worktree remove` **without `--force`**: git refuses a worktree with modified or untracked files (or a locked one), and that refusal is the answer, `409 CONFLICT` with git's own words in `error`. A live session at or inside the worktree is `409` as well. With `deleteBranch=true` the branch is deleted with `git branch -d`, never `-D`, so an unmerged branch is kept: the response is still `200`, with `branchDeleted: false` and git's reason in `branchError`. Response: `{ removed: { name, path, branch, branchDeleted, branchError? } }`. A folder git does not list as a worktree of this repository, or a symlink in its place, is `404`/`400`.
**Starting a session in a worktree** goes through the normal create paths: `POST /api/quick-start` takes `worktree: "<name>"` next to `caseName` and runs the session (and the hooks, the agent skill and `modelOverride`) in the worktree folder; the response adds `worktree` and `workingDir`. It is refused for remote and Docker cases (`400`), for a worktree that is not a registered, managed one (`404`), and for a case that does not exist (`404`: a worktree launch never scaffolds a case). `POST /api/sessions` takes the worktree path as `workingDir` like any other folder.
## CLI management
Read and write the CLI registry (`docs/cli-registry.md`). Every **write** route answers `403 FORBIDDEN` while `cliManagementEnabled` is off (the default), and for a non-admin in multi-user mode. A write that would overwrite a `clis.json` which does not parse, or which has group/world permission bits, is refused with `409 CONFLICT` and a message naming the fix; the file is left untouched.
+14
View File
@@ -448,6 +448,20 @@ Tests: `test/git-clone.test.ts` (pure half exhaustively, plus REAL git against a
Further detail: the synchronous clone request is bounded by `GIT_CLONE_TIMEOUT_MS`.
### Git worktrees of a case
**Git worktrees** (case picker → **Git worktrees…**, the phone case sheet's **Worktrees**, Close Session's **Kill & remove worktree**): a LOCAL case whose folder is the top of a git repository keeps worktrees at `<repo>/.claude/worktrees/<name>` on a new branch (default `worktree-<name>`), so parallel agents each get their own checkout instead of sharing the case folder. That is Claude Code's own `--worktree` layout (measured in 2.1.296: same path, branch `worktree-<name>`, names of `[A-Za-z0-9._-]`, at most 64), and Codeman's names are a strict subset of Claude's. Core in `src/git-worktrees.ts` (pure name/branch gates, `worktree list --porcelain` parsing, exclude text; IO behind an injectable runner); routes in `routes/worktree-routes.ts` (`GET`/`POST /api/cases/:name/worktrees`, `DELETE /api/cases/:name/worktrees/:worktree`).
- ⚠️ **Only `<repo>/.claude/worktrees/<valid-name>` is ever created or removed.** Every component is `lstat`ed (a symlinked `.claude`, `worktrees` or worktree folder is refused, never followed) and the real path must equal the lexical one, so a repository cannot redirect a create or remove outside itself; removal also requires git to list the folder as a worktree of THIS repository. A worktree anywhere else is listed read-only (`managed: false`).
- ⚠️ **git never runs for a case at or inside a Docker case workspace** (or whose common git dir is there), and Docker and remote cases are refused with a 400 before any git, the same rule as the git status indicator: `git worktree add` runs the repository's post-checkout hook and checkout filters ON THE HOST. That is acceptable for a local case, where the user's agent already runs code.
- ⚠️ **Removal is `git worktree remove` WITHOUT `--force`**, refused (409) while any live session (any owner) runs at or inside the worktree, and git's own refusal (modified or untracked files, a lock) is the answer, shown through `textContent`. The optional branch delete is `git branch -d`, never `-D`: an unmerged branch survives and the response says why (`branchError`).
- ⚠️ **The main checkout must not see the worktrees.** When `git check-ignore` says they are not ignored, `/.claude/worktrees/` is appended to the repository's LOCAL `<common git dir>/info/exclude` (never a tracked `.gitignore`), and so is `/.claude/settings.local.json`: Codeman writes its hooks there for every claude session, and an untracked copy in a worktree would make `git worktree remove` refuse forever (Claude Code covers that file with the global excludes file, which a machine may not have). A symlinked `info` dir or `exclude` file is reported, never written through.
- **Names are gated before git**: charset and length (Zod and `worktreeNameError()`), then `git check-ref-format --branch`, whose output must EQUAL the input (`--branch` expands `@{-1}`, which the charset gate already refuses). The base is a resolved commit (`rev-parse --verify <base>^{commit}`), passed after `--` with `--no-track`; an existing branch is refused (`-b`, never `-B`, which would reset it). Create and remove are serialized per repository.
- **Sessions start through the EXISTING create paths**, never a new spawn path: `POST /api/quick-start` with `worktree` (resolved with the same guards; hooks, the agent skill, the model override and the session all target the worktree, and a case that does not exist is a 404, never a scaffold), or `POST /api/sessions` with the worktree as `workingDir` (what the Run button uses for claude and shell). The frontend launches through `run({ worktree })` with a temporary `_runMode` swap, tabs named `w<n>-<worktree>`.
- The unified session list names a live session's worktree from its folder layout (`worktreeFromPath`), so a codex or shell session gets the worktree pill before (or without) a transcript record.
Tests: `test/git-worktrees.test.ts` (pure half, then REAL git: create, exclude, symlink guards, dirty/live/locked refusals, `branch -d`), `test/routes/worktree-routes.test.ts` (real repository through the routes and quick-start, Docker/remote/ownership refusals), `test/worktree-ui.test.ts`, `test/worktree-modal-i18n.test.ts`.
### Ultracode and workflow-run visualization
**Ultracode / Workflow-run visualization** (opt-in `showUltracodeAgents`, default OFF; released 1.1.2): the Workflow tool ("ultracode") writes a COMPLETION artifact per run at `~/.claude/projects/<projHash>/<sessionUuid>/workflows/wf_*.json` (written only at run end); LIVE in-flight runs exist only as transcript dirs at `…/subagents/workflows/wf_<id>/` (journal.jsonl + `agent-*.jsonl`). `workflow-run-watcher.ts` (STANDALONE — deliberately never imports/touches `subagent-watcher.ts`; separate singleton, though it independently reads the same `subagents/workflows/` tree) scans BOTH sources via periodic poll + per-directory chokidar watchers with per-source mtime skip (LRU agentStatCache + journalCache), synthesizing ACTIVE runs (live per-agent tokens/tools/state from transcripts, title/phases from the workflow script) until the completion `wf_*.json` appears and supersedes, and broadcasts SSE `workflow:run_discovered`/`run_updated`/`run_removed`. The watcher is started when **either** `showUltracodeAgents` **or** `ultracodeFloatingWindows` is on (`server.ts` `isWorkflowAgentTrackingEnabled()` returns `(showUltracodeAgents ?? false) || (ultracodeFloatingWindows ?? false)`). Served via `GET /api/workflows` (optional `?minutes=` filter) and `GET /api/workflows/:runId`. Frontend `ultracode-panel.js` renders a docked master-detail view (LEFT: runs + phases; RIGHT: per-agent tokens + tool-calls; click an agent card → its live transcript via client-side `agentId` join). **Additionally**, `ultracode-windows.js` auto-pops a draggable **floating window per active run** (gated on a **DEDICATED** `ultracodeFloatingWindows` toggle, default OFF — independent of the dock panel's `showUltracodeAgents`; see `_ultracodeFloatingEnabled()`), connected by a glowing line to the originating session tab (resolved by `session.claudeSessionId === run.sessionUuid`) — same line idiom as subagent windows, drawn into the shared `#connectionLines` SVG from the tail of `_updateConnectionLinesImmediate`. The window auto-closes ~8s after its run finishes; explicit dismissals are remembered. Clicking an agent card opens an **in-page** connected transcript window (not a browser popup); both run and transcript windows minimize **into** the originating session tab as a merged `ULTRA` badge (🧬 runs / 📄 transcripts) with a restore/dismiss dropdown — minimized runs are skipped by auto-pop. Gesture beta: floating subagent/ultracode windows are pinch-draggable (a `window` grab kind in `entry.ts`). Types: `src/types/workflow-run.ts`. Config: `src/config/workflow-config.ts`.
+2 -1
View File
@@ -46,7 +46,8 @@ case. Each has a stable id, and that id is what the API, the wait primitives, an
event use.
Several sessions can share one case. That is the normal way to parallelize: three workers
in the same repo, three tabs, one case.
in the same repo, three tabs, one case. They then also share one folder; when the workers
edit and commit in parallel, give each its own checkout with [Git Worktrees](Git-Worktrees).
A session carries state the case does not:
+66
View File
@@ -0,0 +1,66 @@
# Git Worktrees
Sessions in one case share its folder. That is fine for one agent, but three agents in the
same checkout edit the same files, overwrite each other's builds, and commit each other's
half-finished work. A **git worktree** gives each of them a checkout of its own: same
repository, same history, a separate folder on a separate branch.
Codeman creates worktrees for a case whose folder is a git repository, starts sessions in
them, and removes them again when the work is done.
## Where they live
`<case>/.claude/worktrees/<name>`, on a new branch `worktree-<name>`. That is exactly where
Claude Code's own `claude --worktree <name>` puts them, so a worktree made in Codeman is one
Claude Code recognizes, and the other way round. Codeman's Recent Sessions and Session Manager
lists mark a session that runs in one with a `⑂ <name>` pill.
The main checkout does not show the worktrees as untracked: Codeman adds `/.claude/worktrees/`
(and `/.claude/settings.local.json`, the hooks file it writes for every Claude session) to the
repository's local `.git/info/exclude` when git does not ignore them already. Your tracked
`.gitignore` is never touched.
## Creating one and starting a session in it
1. Pick the case in the toolbar's case picker.
2. Open the picker again and choose **Git worktrees…** (on a phone: the case sheet's
**Worktrees** button).
3. Under **New worktree**, type a name (letters, digits, `.`, `_`, `-`, up to 64 characters).
4. Pick the CLI under **Start sessions with** (Claude Code, Codex, a shell, any installed run
mode).
5. **Create & start session**.
The worktree is created from the case's current commit, and the session opens in it as a tab
named `w1-<name>`. Under **Branch and start point** you can name the branch yourself or start
from another branch, tag or commit. An existing branch is never reused or reset: pick another
name.
**Open** on a worktree's row starts another session in it, with the CLI chosen above.
## Removing one
Two ways, and both use `git worktree remove` **without** `--force`:
- **Remove…** on the worktree's row in the dialog.
- Closing a session that runs in a worktree: the Close Session dialog offers **Kill & remove
worktree**, which kills the session and then removes its worktree.
git refuses to remove a worktree that still holds uncommitted or untracked files, and Codeman
shows that refusal as git worded it and leaves everything in place. Commit, stash or delete the
work first, then remove again. A worktree a live session still runs in is refused too.
**Also delete its branch** deletes the branch with `git branch -d`, which keeps a branch whose
commits are not merged yet. The worktree is still removed, and the message says the branch was
kept.
## What is not supported
- **Docker cases and remote (SSH) cases.** A container can write to its workspace, and creating
a worktree runs the repository's hooks and checkout filters on the Codeman machine, so git is
never run there. A remote case's repository is on another machine.
- A case folder that is not the top of a repository (a subfolder of one), a bare repository, or
an empty repository with no commit yet.
- Worktrees outside `.claude/worktrees/` (made by hand with `git worktree add`) are listed, but
Codeman neither starts sessions in them from the dialog nor removes them.
The HTTP endpoints are in the [HTTP API](HTTP-API) reference (`/api/cases/:name/worktrees`).
+1 -1
View File
@@ -72,7 +72,7 @@ Roughly 235 handlers across 26 route modules. By domain:
| ------------------- | -------- | --------------------------------------------------- |
| System | 56 | Status, settings, digest, updates, tunnel. |
| Sessions | 34 | Create, input, terminal, wait, last response, kill. |
| Cases | 34 | Create, link, clone, remote and docker cases. |
| Cases | 37 | Create, link, clone, worktrees, remote and docker cases. |
| Files | 17 | Preview, edit, raw, attachments, path picker. |
| Orchestrator | 10 | Plans and phases. |
| Ralph | 9 | Loop control and configuration. |
+1
View File
@@ -66,6 +66,7 @@ codeman web # then open http://localhost:3000
| [Agent CLIs](Agent-CLIs) | Which agent should this session run, and how do I set it up? |
| [Custom Model Endpoints](Custom-Model-Endpoints) | How do I point a session at my own OpenAI-compatible endpoint? |
| [Working With Files](Working-With-Files) | How do I read, edit, and attach files? |
| [Git Worktrees](Git-Worktrees) | How do several agents work on one repository without sharing a folder? |
| [Input And Voice](Input-And-Voice) | How do I talk to an agent, including by voice? |
| [Mobile Guide](Mobile-Guide) | How well does this work on a phone? |
| [Keyboard Shortcuts](Keyboard-Shortcuts) | What can I drive from the keyboard? |
+1
View File
@@ -15,6 +15,7 @@
- [Agent CLIs](Agent-CLIs)
- [Custom Model Endpoints](Custom-Model-Endpoints)
- [Working With Files](Working-With-Files)
- [Git Worktrees](Git-Worktrees)
- [Input And Voice](Input-And-Voice)
- [Mobile Guide](Mobile-Guide)
- [Keyboard Shortcuts](Keyboard-Shortcuts)