Development record
Lab finding lab-260831162806-976575f, probe p5b-live-session-retest: the PreToolUse guard payload carries only tool_input.command, and a destructive command moved into the tool's workdir slips the match. (1) Contract fact: `guardHookInput` (internal/surface/cli/guard.go) reads cwd, tool_name and tool_input.command, and cwd is used only for rulesRoot; a host whose shell tool has a second execution-shaping input (opencode's `workdir`; any host passing a per-call cwd or env) lets a model move meaning out of the string the guard reads. (2) Evidence: the lab's plugin-guard.log records 18:36:58 allow exit 0 for `rm -rf *`, then 18:37:40 block exit 2 for `cd scratch && rm -rf *` — the model had rewritten the chain into workdir=scratch plus `rm -rf *`. (3) The bypassed entry is rm-rf-after-cd-chain, whose why is that a FAILED cd leaves the delete running wherever the shell already was; bare `rm -rf *` is not a registry hazard by design (its successor is an absolute-path delete as its own command). A host-honoured workdir is a different mechanism from a shell cd: whether the failed-cd hazard exists at all depends on what the host does when workdir does not exist — fail the tool call (no hazard) or fall back to the session directory (the hazard) — and the probe evidence does not answer that, so it must be probed on that host before either branch is chosen. (4) No shipped surface carries the field today: hooks/hooks.json wires the Bash matcher only, and the shipped host's Bash tool input has command, timeout, description, run_in_background; the opencode adapter is lab-tier (itd-22 lineage), nothing of it in the tree. (5) The fork, for the maintainer: (A) extend the guard contract — guard.Check in core gains a context carrying the host's workdir/cwd and entries may constrain it; the naive form where the hook prepends `cd <workdir> && ` before Check is WRONG and must not be the implementation, because it fires the cd-chain entry on every workdir'd recursive delete, whose failed-cd hazard the host-managed chdir does not have; versus (B) document the reach — add to the guard brief chapter's 'what an allow still does not see' and to adr-42's scope that the guard reads the command string, a host input that changes where or as whom it runs is outside the read, and a host adapter is responsible for folding such inputs into the string it submits or into a documented field. adr-42 decision 1 (mistake filter; the execution-layer boundary belongs to the host, adr-25) leans toward B, but A is a core API change the ADR does not settle. (6) Severity minor, category observation; stays open until the maintainer picks.
Lab finding lab-260831162806-976575f, probe p5b-live-session-retest: the PreToolUse guard payload carries only tool_input.command, and a destructive command moved into the tool's workdir slips the match. (1) Contract fact: guardHookInput (internal/surface/cli/guard.go) reads cwd, tool_name and tool_input.command, and cwd is used only for rulesRoot; a host whose shell tool has a second execution-shaping input (opencode's workdir; any host passing a per-call cwd or env) lets a model move meaning out of the string the guard reads. (2) Evidence: the lab's plugin-guard.log records 18:36:58 allow exit 0 for rm -rf *, then 18:37:40 block exit 2 for cd scratch && rm -rf * — the model had rewritten the chain into workdir=scratch plus rm -rf *. (3) The bypassed entry is rm-rf-after-cd-chain, whose why is that a FAILED cd leaves the delete running wherever the shell already was; bare rm -rf * is not a registry hazard by design (its successor is an absolute-path delete as its own command). A host-honoured workdir is a different mechanism from a shell cd: whether the failed-cd hazard exists at all depends on what the host does when workdir does not exist — fail the tool call (no hazard) or fall back to the session directory (the hazard) — and the probe evidence does not answer that, so it must be probed on that host before either branch is chosen. (4) No shipped surface carries the field today: hooks/hooks.json wires the Bash matcher only, and the shipped host's Bash tool input has command, timeout, description, run_in_background; the opencode adapter is lab-tier (itd-22 lineage), nothing of it in the tree. (5) The fork, for the maintainer: (A) extend the guard contract — guard.Check in core gains a context carrying the host's workdir/cwd and entries may constrain it; the naive form where the hook prepends cd <workdir> && before Check is WRONG and must not be the implementation, because it fires the cd-chain entry on every workdir'd recursive delete, whose failed-cd hazard the host-managed chdir does not have; versus (B) document the reach — add to the guard brief chapter's 'what an allow still does not see' and to adr-42's scope that the guard reads the command string, a host input that changes where or as whom it runs is outside the read, and a host adapter is responsible for folding such inputs into the string it submits or into a documented field. adr-42 decision 1 (mistake filter; the execution-layer boundary belongs to the host, adr-25) leans toward B, but A is a core API change the ADR does not settle. (6) Severity minor, category observation; stays open until the maintainer picks.
Grounds
- pursued: the failed-cd hazard does not exist for a host-managed workdir on the probed host; a probe of a later host version that falls back to the session directory would show it wrong
Frontmatter
| id | iss-2609012040019014 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609012040019014-lab-finding-lab-260831162806-976575f.md
open on GitHub ↗ · commit history ↗
mentions4
- adr-25The LLM is host-delegated by default; oracles are opt-in adapters
- adr-42The guard's parse layer is a mistake filter, not a security boundary — matching is two-tier, and completeness is abandoned as a goal
- dcn-26092800000000032026-09-28 — guard registry, the rm targets (iss-2609282105242542, autonomous run A lane gateFix): the chapter's headline promised the catch of "an `rm -rf` with an unlucky glob" while the one rm entry fired only behind a `cd` chain, so `rm -rf /` and `rm -rf ~` were an allow. Two bundled entries close it through one new additive Pattern field, `arg_values` (some operand is exactly one of the listed words, by its known text; empty and dash-led values are load-time rejections like an empty prefix). `rm-rf-root-or-home` is a BLOCKER on `/`, `/*`, `~`, `$HOME`, `${HOME}` and their `/` and `/*` forms; `rm-rf-working-directory` is a WARN on `*`, `.`, `..`, `./`, `../`, `./*`, `../*` and `.*`, graded like `git clean` because emptying a build directory the same way is ordinary work and a blocker there would teach sessions to route around the guard. Both require the recursive flag alone, not `-f`: for an agent, whose stdin is not a terminal, rm does not prompt, so `-f` changes nothing about what is destroyed (the cd-chain entry keeps its `-f` requirement; changing a blocker's pattern is a separate act). This reverses the incidental stance that a bare `rm -rf *` is not a hazard (the lab finding iss-2609012040019014 and two tests that used it as their ordinary-work probe): those tests keep their point with `rm -rf ./build` as the probe, and a host workdir is still never read as a `cd`. A third operand residual is recorded beside the two unknown.go names: a target a substitution prints WHOLE (`rm -rf $(echo /)`) is read by its known text, as the `+` refspec prefix is, because `rm -rf $(find . -name '*.pyc')` is how an everyday delete names its targets and reading the word as every target would block it as a delete of `/`; glued text still counts (`"$(true)"/` is `/`). Parameter expansions stay unread except as the literal words the entry names (`rm -rf "$DIR"/*` with `DIR` unset is not seen). The per-byte work bar of the guard's cost tests moves from 20 to 24: the operand walk reads every token once per entry, and sixteen entries put the costliest asserted shape at about 20.
- itd-22abcd Reaches Any Harness Through an Adaptor