Worktree isolation refuses read-only commands with no git invocation; measured cost is command fragmentation, not the refusals
Summary
In a worktree-isolated session, the Bash guard refuses commands it cannot
statically prove stay inside the worktree — including read-only commands that
contain no git invocation at all. The refusals themselves are cheap. The
expensive part is the adaptation: an agent that hits them writes much shorter,
much plainer commands for the rest of the session, and every extra round trip
costs about 7.6 seconds.
I measured this across 1,108 local session transcripts (60,043 Bash tool calls)
because a pipeline that used to take ~15 minutes per unit of work had grown to
2+ hours, and I wanted to know how much of that was this. Figures and method
below so they can be checked or contradicted.
Environment
- Claude Code in the VS Code extension
- macOS 25.5.0 (arm64), bash 3.2
- Sessions isolated via
EnterWorktree
The refusal
This session is isolated in the worktree <path>, but this command is too
complex to verify that it stays inside the worktree. Refusing to run it — a
worktree-isolated session's git operations must target its own worktree.
False positives are easy to hit. Three from a single session, none of which
touches git:
- A
python3 - <<'PY' ... PYheredoc that reads JSON files and prints
medians. Read-only, no git, no writes. This one is the sharpest: the message
says "git operations must target its own worktree" about a command
containing no git invocation.
bash tests/foo.test.sh 2>&1 | tail -30— a pipe totail.printf ... > scratch/a.sh && chmod +x scratch/a.sh && eval "$CMD"— writes
confined to a scratch directory.
Each costs two extra round trips: write the payload to a file, then run the file.
Measured cost
The refusals are not the problem. 378 across the corpus — 0.63% of all Bash
calls, 3–7% within worktree sessions. Median recovery 4.0s. Roughly 57 minutes
in total, across all history.
The adaptation is. Comparing Bash calls within the same session, before vs
after that session's EnterWorktree call:
| | before | after |
|---|---|---|
| median command length | 490 chars | 123 (−75%) |
| compound share (&&, \|\|, ;) | 77.7% | 41.9% (−35.8 pts) |
Control, which inverts. Sessions that never entered a worktree (n=178),
split at their first Edit/Write — the same exploration→implementation
boundary — move the opposite way: +49% longer, +2.8 pts more compound.
So "commands naturally get shorter once you start implementing" does not explain
the treatment group. It predicts the reverse of what happens.
Why shorter costs more. Measured round-trip latency, taken as the gap from a
Bash call to the next tool call (capped at 120s to exclude human pauses):
| | seconds |
|---|---|
| round trip, short command (<150 chars, execution ≈ 0) | 7.6 |
| round trip, long command | 10.3 |
| marginal cost of adding work to an existing command | 2.7 |
Four operations in one compound command cost ~10s. The same four as separate
calls cost ~30s — a 3× penalty.
Estimated total fragmentation cost: 2.9–11.1 min/session. It is a range
rather than a number because "the same work expressed in more calls" is not
directly observable — only the shape change is — and the length distribution is
skewed (median 490, mean 974), so the two defensible estimators give 1.17× and
2.17×. The shape change itself is measured directly and is not an estimate.
For scale: a median worktree session in this corpus makes 224 Bash calls =
28.5 minutes of pure round-trip latency, about 17% of a 175-minute session.
Suggestions, roughly by value
- Let obviously read-only commands through. No write redirection, no git
subcommand, no mutating binary — not a worktree-escape risk. The heredoc case
is the clearest miss.
- Say what specifically could not be verified. "Too complex" gives nothing
actionable, so the adaptation is "write everything shorter" rather than "avoid
this one construct." Naming the offending token would let an agent keep
compound commands and drop only the unverifiable part. I suspect this single
change would remove most of the measured fragmentation.
- Allow pipes to well-known read-only sinks (
tail,head,wc,sort,
grep) without re-triggering the analysis.
- Consider a per-command explicit opt-out for cases the analyser cannot
decide, instead of a blanket refusal.
Reproducing
The analysis reads ~/.claude/projects/*/*.jsonl. For each transcript:
- classify the session by whether it called
EnterWorktree— inspect
tool_use blocks, not raw text. The tool name appears in the
deferred-tool listing of nearly every transcript; a raw-text grep misclassified
984 of 1,210 sessions in my first attempt and silently emptied the control arm.
- split its Bash commands at that call; compare median length and compound share
before vs after.
- for the control, take sessions that never called it and split at the first
Edit/Write.
- for the tax, take the gap from each Bash call to the next tool call, bucketed
by whether the command is under 150 characters.
Happy to share the script if useful.
Caveat
The 2.9–11.1 min/session figure is an estimate, stated as a range for the reason
above. The shape change (−75% length, −35.8 pts compound, against a control that
moves +49%) and the 7.6s/10.3s round-trip costs are direct measurements.