Development record
2026-08-01 — guard batch (iss-159/144/148), FIX pass after two fresh adversarial reviews returned BLOCK (security) and FIX FIRST (correctness). Detector-first throughout: each fix has a test watched failing before it and passing after. Two claims in the entry above are superseded by it. First, "backtick substitution reaches command position through the same segment flush the grouping parens already gave `$( … )` by accident" — the flush was the bug, not the mechanism. `flushSegment` permanently ENDS the enclosing command's segment, so every flag and operand written after a substitution closed started a new, unrelated command: `cd scratch && rm ` + a backticked substitution + ` -rf *` went from block on main to allow on this branch, a regression this branch introduced, and the identical hole already existed for `$( … )` on main (`rm $(true) -rf *`). Both are fixed together rather than one fixed and one documented, because the mechanism to fix them is the same: a substitution pushes a FRAME that parks the enclosing tokens, accumulates its own content, flushes that as its own segments (a hazard inside a substitution is still matched — the point of the original fix), and restores the enclosing tokens so the segment resumes. Segments come back in SOURCE order rather than at the close boundary, which is what keeps `precededByCD` reading a cd written before a substitution as preceding it. A bare `(` deliberately keeps the old flush: it is a subshell group, not a substitution inside a command, and it genuinely ends the token run before it — `f() { rm -rf *; }` would otherwise glue the body onto the function's name and lose the `rm` from command position. Imbalance degrades rather than errors (a close with no open falls back to the plain flush; an unterminated span is unwound at end of input rather than dropped, since a hazard inside it still executes), consistent with the file's existing philosophy that only an unterminated quote or heredoc — which changes what the REST of the input means — is `ErrUnparsableCommand`; a real shell rejects unbalanced parens itself, and this guard's job is to refuse to misparse into an allow, not to validate shell grammar. Second, "an api path written as a fully-qualified URL is NOT matched (a stated limit)" — it is matched now. `gh` passes an absolute URL through to the API unchanged, so the URL form was a real bypass of a real repository deletion, and documenting it was the wrong end: an operand is normalised to its path (scheme/host, query, fragment dropped) before the depth check, which only ever shortens, so the depth limit that keeps deeper work allowed is untouched. The host is deliberately NOT inspected — deciding which hostnames are the real API is a lookalike-domain problem this guard cannot settle, and normalising every authority away can only make the check see a path it would otherwise have missed. What is left is narrower and now disclosed in all four surfaces rather than in a Go comment and a test: a host that MOUNTS the API under a prefix (GitHub Enterprise Server's `/api/v3/`) is not seen, because matching a root segment wherever it appeared would falsely refuse `DELETE /teams/{id}/repos/{owner}/{repo}`, which removes a repository from a team and destroys nothing. Third item, no supersession: `Validate` rejected a dashed `subcommand`/`subcommand2` but not the two operand fields this branch added, so `"arg_prefixes": ["-f"]` and an `arg_paths` root carrying a slash both loaded clean and could never fire — the "looks armed, never fires" class the validator exists to catch. Both are load-time rejections now, with the well-formed shapes pinned as accepted so the checks cannot quietly take the fields out of use.
2026-08-01 — guard batch (iss-159/144/148), FIX pass after two fresh adversarial reviews returned BLOCK (security) and FIX FIRST (correctness). Detector-first throughout: each fix has a test watched failing before it and passing after. Two claims in the entry above are superseded by it. First, "backtick substitution reaches command position through the same segment flush the grouping parens already gave $( … ) by accident" — the flush was the bug, not the mechanism. flushSegment permanently ENDS the enclosing command's segment, so every flag and operand written after a substitution closed started a new, unrelated command: cd scratch && rm + a backticked substitution + -rf * went from block on main to allow on this branch, a regression this branch introduced, and the identical hole already existed for $( … ) on main (rm $(true) -rf *). Both are fixed together rather than one fixed and one documented, because the mechanism to fix them is the same: a substitution pushes a FRAME that parks the enclosing tokens, accumulates its own content, flushes that as its own segments (a hazard inside a substitution is still matched — the point of the original fix), and restores the enclosing tokens so the segment resumes. Segments come back in SOURCE order rather than at the close boundary, which is what keeps precededByCD reading a cd written before a substitution as preceding it. A bare ( deliberately keeps the old flush: it is a subshell group, not a substitution inside a command, and it genuinely ends the token run before it — f() { rm -rf *; } would otherwise glue the body onto the function's name and lose the rm from command position. Imbalance degrades rather than errors (a close with no open falls back to the plain flush; an unterminated span is unwound at end of input rather than dropped, since a hazard inside it still executes), consistent with the file's existing philosophy that only an unterminated quote or heredoc — which changes what the REST of the input means — is ErrUnparsableCommand; a real shell rejects unbalanced parens itself, and this guard's job is to refuse to misparse into an allow, not to validate shell grammar. Second, "an api path written as a fully-qualified URL is NOT matched (a stated limit)" — it is matched now. gh passes an absolute URL through to the API unchanged, so the URL form was a real bypass of a real repository deletion, and documenting it was the wrong end: an operand is normalised to its path (scheme/host, query, fragment dropped) before the depth check, which only ever shortens, so the depth limit that keeps deeper work allowed is untouched. The host is deliberately NOT inspected — deciding which hostnames are the real API is a lookalike-domain problem this guard cannot settle, and normalising every authority away can only make the check see a path it would otherwise have missed. What is left is narrower and now disclosed in all four surfaces rather than in a Go comment and a test: a host that MOUNTS the API under a prefix (GitHub Enterprise Server's /api/v3/) is not seen, because matching a root segment wherever it appeared would falsely refuse DELETE /teams/{id}/repos/{owner}/{repo}, which removes a repository from a team and destroys nothing. Third item, no supersession: Validate rejected a dashed subcommand/subcommand2 but not the two operand fields this branch added, so "arg_prefixes": ["-f"] and an arg_paths root carrying a slash both loaded clean and could never fire — the "looks armed, never fires" class the validator exists to catch. Both are load-time rejections now, with the well-formed shapes pinned as accepted so the checks cannot quietly take the fields out of use.
Frontmatter
| id | dcn-2608010000000003 |
|---|---|
| type | decision |
| date | 2026-08-01 |
.abcd/work/decisions/dcn-2608010000000003-guard-batch-iss-159-144-148-fix-pass.md
open on GitHub ↗ · commit history ↗
mentions1
- 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.