---
name: a-bare-cd-silently-repoints-every-later-read
description: "A single `cd` inside one Bash call persists for the rest of the session, so later greps read a DIFFERENT checkout than the one you are editing — and their output looks identical either way."
metadata: 
  node_type: memory
  type: feedback
  originSessionId: c4bdac5d-1a80-4752-8954-99d14618d308
  modified: 2026-08-22T05:37:27.776Z
---

Working the W1 lane in `.worktrees/keystone-w1`, I ran one command as
`cd /c/.../spt-core && cat body | spt send doyle` — just to send a report. That `cd` persisted:
every later `grep`/`sed` read the PROJECT ROOT, which sat on `main` @`80ab77a`, while my Edits used
absolute lane paths and landed correctly on `cc2b09b`. I read `build_access_rule`, `apply_mutation`
and `open()` out of a tree that was missing T1 and T2, and reasoned about the arm I was fixing from
the wrong sha. Caught only because a `grep -rn ... .` printed a path beginning `./.worktrees/` —
a path the lane's own cwd could never produce.

**Why:** the Bash tool's cwd persists across calls, and a `cd` buried in a compound command for an
unrelated purpose (sending a message, running a helper from the root) does not read as a *state
change*. Nothing in a later grep's output names its root — the same file at two shas prints the same
kind of content, so a stale read is indistinguishable from a fresh one right up until a line number
does not match.

## RECURRENCE ×3 IN ONE SESSION, BY A DIFFERENT AGENT, WITH THIS ENTRY INDEXED AT ⭐⭐ THE WHOLE TIME (doyle, 2026-08-22)

TURNKEY #212. This entry is in `MEMORY.md`, two-star, and loads every session. It did not fire
three separate times: (1) `cd .worktrees/assembly-212` for a cherry-pick, so the next call's
`.worktrees/...` path failed to resolve; (2) `cd` into the assembly worktree again, so a
`traceable-reqs check` I read as the assembly's verdict had actually run in the ROOT — I caught
that one and re-ran in a subshell; (3) committing IR-55, where a `git checkout -- <path>` intended
for the ROOT ran in the assembly worktree instead. The third was harmless ONLY because the file was
already committed there — the same command one minute earlier would have destroyed the uncommitted
entry it was meant to protect.

**Why a ⭐⭐ entry still did not fire, which is the useful part:** the rule as written is a
*remember-to* — "use `-C`, and `pwd` after any `cd`". Remembering happens AFTER the `cd` is already
typed, and the `cd` is never the point of the command; it is scaffolding for the real verb, so it
does not present itself as a decision. A rule that depends on noticing an act you perform without
noticing will keep failing at exactly this rate.

**STRUCTURAL RESTATEMENT, which is the operative form:** never write a bare `cd` in a tool call at
all. Use `git -C <dir>`, absolute paths, or — when a command genuinely needs a working directory —
**wrap it in a subshell: `( cd <dir> && … )`**, which makes the persistence physically impossible
rather than something to remember not to do. I used the subshell form correctly once in the same
session, so this is not a knowledge gap either; it is that only the subshell removes the need for
vigilance. Prefer the form that cannot fail over the habit that must not.

**Kin, and they are the same shape:** deployah's 2026-08-22 catch that "the file is git-tracked"
names the FILE'S TRACKING STATUS and not the CONTENT'S COMMITTED STATE; counting `pipefail`
occurrences names the GUARD and not the EXPOSURE; a filter's match count names the FILTER and not
the COVERAGE. A `cd`'s success names the PATH'S RESOLUTION and not the TREE it resolved in. Four
instances in one day of a claim naming the wrong property — see
[[dont-take-a-diagnosis-as-measured]]. **The fix is always the same: name the thing you measured.**

**How to apply:** never `cd` out of the lane inside a compound command — use `-C <dir>` (git),
absolute paths, or accept the extra call. After any command containing `cd`, re-establish the lane
cwd explicitly and `pwd` it. When a source read is going to carry an argument, confirm the tree it
came from at the moment you read it: `git rev-parse HEAD` from the same cwd, per
[[source-read-needs-its-sha-and-ancestry]]. A worktree makes this sharper, not safer — two
checkouts of the same PROJECT at different shas is exactly the setup where a stale read stays
plausible for a long time. Kin [[background-cd-relative-trap]] (the background-task face of the same
hazard) and [[check-branch-before-commit-shared-checkout]].

## The dangerous face is a WRONG-TREE VERDICT (deployah, 2026-08-22)

doyle hit this three times in one session with the ⭐⭐ rule already in his index. Two failed loudly
(a relative path that did not resolve; a `git checkout --` that was harmless only because the file
was already committed). **The middle one is the one to fear: a `traceable-reqs check` he read as the
ASSEMBLY head's verdict had actually run in the ROOT checkout.** He caught it and re-ran it in a
subshell — but a wrong-tree verdict reads EXACTLY like a right-tree verdict. Same command, same
formatting, same exit 0. Nothing about it is anomalous, so there is no tell to notice, which puts it
beyond the reach of any remember-to rule.

This is the release lane's exposure specifically, because intake and gate verdicts are the whole job:
a green `traceable-reqs`, a clippy count, a suite total, a `--version` render — every one of them is
a claim ABOUT A TREE, and none of them says which tree in its own output.

**Practice: make a verdict carry its tree in the same call.** Not a separate `pwd` afterwards, which
is another remember-to and can drift from the command it was meant to describe:

    ( cd <tree> && git rev-parse --short HEAD && git status --porcelain | head -3 && <the verdict command> )

One call, one subshell, and the sha is printed by the same invocation that produced the verdict — so
the verdict cannot be quoted later without its provenance attached, and a dirty tree is visible at the
moment the number is made rather than assumed away. Cite the pair (sha + verdict), never the verdict
alone. Kin [[read-source-at-the-measured-sha-not-the-default-tree]] — same hazard on the read side;
this is its verdict side.

**THIRD FACE, and the worst one: the cwd breaks a `&&` CHAIN and the FINAL STEP STILL REPORTS SUCCESS
(doyle, 2026-09-06, IR-76).** A relative `cd` failed because the cwd had already been repointed, the
`&&` chain therefore SKIPPED the commit — and the `git push` at the end ran anyway and SUCCEEDED,
publishing an EMPTY branch at the base sha. The push output is indistinguishable from a real one: a
new-branch line, a tracking line, exit 0. Nothing says "you pushed nothing."

This is the class's write side, and it inverts the usual defence. The read side (above) is caught by
making a verdict carry its tree. Here there IS no wrong-tree verdict to catch — the step that would
have produced the artifact never ran, so there is no output to inspect, and the LAST command in the
chain is the one that speaks. **A chain's exit status describes its last executed step, never its
intended steps.**

**Detector: read the SHA, never the push output — and read it on the REMOTE.** `git fetch` then
`git rev-parse origin/<branch>` (or the gh branch API) naming your commit is the proof; a clean push
is not. **Read the remote ref, NOT the local one** (doyle's refinement, and it is strictly stronger
than the local form this entry first carried): a LOCAL `git log --oneline -1 <branch>` names your
commit in the adjacent variant where the commit SUCCEEDED and the push did not carry it — so the
weaker detector reports success in exactly the case the stronger one exists to catch. Verify the ref
you are making a claim about, which is the published one. Same shape as [[gh-run-list-commit-needs-a-full-sha]] — a confident, well-formed,
successful-looking report about a thing that is not there.

**Frequency datum, which is why this is a mechanism and not an anecdote: FOUR instances in ONE
session across TWO agents (2026-09-06).** todlando x3 (all read-side: source read from the root tree
while edits landed on the lane; result files reported "not found"; a `fire_echo` read as unmodified
when the lane's copy was correct) and doyle x1 (write-side, above). Both of us had this entry banked.
Reading it does not apply it — the cwd persists across tool calls, so the trap is armed by DEFAULT and
disarmed only by never relying on it. Use `git -C <abs>` / absolute paths in every command, or a
`( cd … && … )` subshell that cannot leak.


**WRITE-SIDE FACE (2026-09-06, mine; todlando named the inversion):** cwd persisted inside
`.worktrees/ir76` from the previous call; the next call's relative `cd .worktrees/ir76 && git commit`
failed at the cd, the `&&` chain SKIPPED the commit, and `git push -u origin <branch>` on the next
line pushed an EMPTY branch at main's sha — new-branch line, tracking line, exit 0, indistinguishable
from a real push. The read-side defence ("make a verdict carry its tree") is useless here: the step
that would have produced the artifact never ran, so there is no wrong-tree verdict to inspect; the
chain's exit status described its LAST EXECUTED step. A second recurrence 3 min later: a block-compare
read the worktree's own file as "main" and my exit test checked `echo`, not python. FOUR instances,
ONE session, TWO agents, entry already banked by both — reading it does not apply it. The trap is
armed by default (cwd persists across calls) and disarmed only by never relying on cwd: `git -C <abs>`,
absolute paths in every argv, or a subshell `( cd … && … )` that cannot leak. Detector for any
push: `git rev-parse origin/<branch>` (after fetch) or the gh branch API names YOUR commit — a clean
push is not proof. Kin: [[never-send-a-claim-composed-before-its-check-ran]].
