Development record
2026-08-01 — guard round (iss-148, iss-159, iss-144): the matcher's wrapper walk now steps over a wrapper's OWN arguments rather than its name alone, off two explicit tables — the flags each wrapper documents as taking a value, and the mandatory operand no flag stepping can reach (`timeout [OPTIONS] DURATION COMMAND...`, where `timeout 30 rm -rf /` read as a command called `30`). That was the sharpest of the three items and the reason to take it first: `sudo -u bob <hazard>` read `-u` as the command name, so one extra token turned an entry the registry DOES describe into an allow, and a guard that has stopped refusing looks identical to one with nothing to refuse. The complementary miss is deliberate and now stated rather than implied — a value flag the table does not name (a bundled `sudo -Hu bob`) is not stepped over, and a miss is a non-match, never a false block. `xargs`, `timeout` and `exec` joined the wrapper set. Backtick substitution reaches command position through the same segment flush the grouping parens already gave `$( … )` by accident, scoped to parity with today's behaviour (unquoted only) rather than POSIX completeness, so the two spellings of one substitution stop disagreeing. Registry content took four additive Pattern fields, each optional and per-repo overridable like the rest: `subcommand2` for a two-level grammar (`gh repo list` and `gh repo delete` share their first level and only one is a hazard), `flag_values` for a flag's SETTING rather than its presence (presence alone would force a choice between missing `-X DELETE` and refusing `-X GET`; the value is compared without regard to case, since a constraint a change of case walks past is not one), `arg_paths` for a root segment AND an exact depth (the depth is the entry's scope: `repos/{owner}/{repo}` IS the repository, while DELETE on a branch ref under it stays ordinary work), and `arg_prefixes` for a hazard carried by an operand with no flag to look at (`git push origin +main:main`). The empty form of each is a load-time rejection, because an empty flag group defangs and an empty prefix over-blocks, and both are invisible in the file. Two behaviours are pinned as tests rather than left to be rediscovered: an api path written as a fully-qualified URL is NOT matched (a stated limit, the family of the payload gap), and `--help` is not an exemption anywhere in this registry — exempting it would mean teaching every entry which of its flags mean 'do nothing', and a guard that reasons about intent can be argued out of refusing. iss-144 needed no code at all: its incident shape was already known-bad fixture #1 and the incident capture already known-good #1, so the only change was adding the three-step form it actually occurred in (mkdir → cd → `rm -rf *`) to the corpus, making the resolve verifiable rather than asserted. Residual and deliberately not acted on: a `cd` in one tool call and an `rm -rf *` in the next is still an allow, since the tokenizer never sees two invocations as one candidate — a harness-level concern, not a registry one.
2026-08-01 — guard round (iss-148, iss-159, iss-144): the matcher's wrapper walk now steps over a wrapper's OWN arguments rather than its name alone, off two explicit tables — the flags each wrapper documents as taking a value, and the mandatory operand no flag stepping can reach (timeout [OPTIONS] DURATION COMMAND..., where timeout 30 rm -rf / read as a command called 30). That was the sharpest of the three items and the reason to take it first: sudo -u bob <hazard> read -u as the command name, so one extra token turned an entry the registry DOES describe into an allow, and a guard that has stopped refusing looks identical to one with nothing to refuse. The complementary miss is deliberate and now stated rather than implied — a value flag the table does not name (a bundled sudo -Hu bob) is not stepped over, and a miss is a non-match, never a false block. xargs, timeout and exec joined the wrapper set. Backtick substitution reaches command position through the same segment flush the grouping parens already gave $( … ) by accident, scoped to parity with today's behaviour (unquoted only) rather than POSIX completeness, so the two spellings of one substitution stop disagreeing. Registry content took four additive Pattern fields, each optional and per-repo overridable like the rest: subcommand2 for a two-level grammar (gh repo list and gh repo delete share their first level and only one is a hazard), flag_values for a flag's SETTING rather than its presence (presence alone would force a choice between missing -X DELETE and refusing -X GET; the value is compared without regard to case, since a constraint a change of case walks past is not one), arg_paths for a root segment AND an exact depth (the depth is the entry's scope: repos/{owner}/{repo} IS the repository, while DELETE on a branch ref under it stays ordinary work), and arg_prefixes for a hazard carried by an operand with no flag to look at (git push origin +main:main). The empty form of each is a load-time rejection, because an empty flag group defangs and an empty prefix over-blocks, and both are invisible in the file. Two behaviours are pinned as tests rather than left to be rediscovered: an api path written as a fully-qualified URL is NOT matched (a stated limit, the family of the payload gap), and --help is not an exemption anywhere in this registry — exempting it would mean teaching every entry which of its flags mean 'do nothing', and a guard that reasons about intent can be argued out of refusing. iss-144 needed no code at all: its incident shape was already known-bad fixture #1 and the incident capture already known-good #1, so the only change was adding the three-step form it actually occurred in (mkdir → cd → rm -rf *) to the corpus, making the resolve verifiable rather than asserted. Residual and deliberately not acted on: a cd in one tool call and an rm -rf * in the next is still an allow, since the tokenizer never sees two invocations as one candidate — a harness-level concern, not a registry one.
Frontmatter
| id | dcn-2608010000000002 |
|---|---|
| type | decision |
| date | 2026-08-01 |
.abcd/work/decisions/dcn-2608010000000002-guard-round-iss-148-iss-159-iss-144-the.md
open on GitHub ↗ · commit history ↗
mentions3
- iss-144shell-hazard incident, first corpus entry: in another locally managed repo, an agent ran mkdir -p under a scratch dir, then cd, then rm -rf * in one && chain. The && made it accidentally safe (rm only after a successful cd); with separate statements or a ; separator, a failed cd would have left the harness's persistent cwd at the repo root and rm -rf * would have deleted the working tree. Failure class: destruction chained after cd + cwd-relative glob, one character between harmless and catastrophic. Safe successor: destructive commands name an absolute path and never rely on cwd or a glob. This incident and the agent's own post-mortem are the first acceptance-corpus entry for the shell-hazard registry (teach + enforce planes); guard fixture must prove the block fires on exactly this shape.
- iss-148guard registry coverage gaps found while wiring itd-103 (registry content, not matching semantics): a push whose refspec carries a leading plus is a force in disguise and no entry describes it; xargs, timeout and exec are absent from the matcher's wrappers set, so a hazard launched through one of them is not seen; a backtick command substitution is not followed, while the dollar-paren form is; and a wrapper that IS in the set defangs an entry the moment it carries its own flags, because only the wrapper name is stepped over — `sudo <hazard>` is seen, `sudo -u bob <hazard>` is not, and the same holds for `env -i` and `time -p`. That last one is the sharpest: it turns an entry the registry does describe into an allow with one extra token, and it is the only item here that a facilitator would reasonably assume was covered. Candidates for the admission gate as the registry grows from reality; the wrapper-flag item is matcher-side (`commandOf` in internal/core/guard/match.go), not registry content.
- iss-159hazard registry has no entries for destructive GitHub remote operations: 'gh repo delete' and 'gh api -X/--method DELETE repos/{owner}/{repo}' both verdict allow. Field evidence: the user-level guard-bash.sh hook blocks both (repo deletion is human-only; second layer behind the missing delete_repo scope) — that hand-wired hook is the prior art to lift into the bundled registry defaults, same prototype-to-product path as itd-74. Registry entries should mirror the hook's nuances: quote-tolerant token matching (already native), depth-limited api paths (DELETE on deeper repo subpaths stays allowed), and the block message naming the human-only successor.