You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
guardrails 2.x: detect tampering of plugin-owned state instead of parsing every shell spelling (OC-N6) #354
Owner decision (Terada Kousuke, 2026-10-25), verbatim: 「(S) + (P)でいきましょう。」 — (S) ship #353 round 3 at a8d90ac299 after a regression-only acceptance; (P) move the guarantee for .opencode/guardrails/state.json from prevention by parsing shell text to detection of any change.
Why
#353 rounds 1–3 (deny-list, then the positive shape of ADR-012) and 8 Codex route-A passes found 14 P1 spellings in total that reach sed or a writer through quoting, escapes, ANSI-C quotes, comments, line continuations and command substitution. Each fix narrowed the guard; none converged. A shell cannot be judged from its text with a bounded rule set, so the text guard stays as early, best-effort refusal and is not repaired further (ADR-012 consequences, aidd-governance). The guarantee has to come from a layer where the problem is finite: the file either changed or it did not.
Proposal
Every write the plugin makes to state.json also records sha256(state.json) (and mtime) in a plugin-private location outside the project, e.g. $XDG_STATE_HOME/opencode/guardrails/<project-id>.integrity.json (the opencode2 launcher's XDG state dir, not inside the repo and not under .opencode/).
Every read of state (stash(state) in guardrail-context.ts) compares the file's hash with the recorded one before trusting it.
Match: proceed.
Mismatch, or no record: treat the state as reviewed=false, factcheck=false, edits_since_review unknown (the conservative values), rewrite it, append a ledger row event=block decision=deny rule=state.tampered with the two hashes, and mark last_block: "state.tampered".
The integrity record is keyed by project id, so a worktree or a copy of the repo does not inherit another's record.
A tamper is also surfaced to the user the same way the headless launcher shows H17 verdicts (ADR-010: ledger tail), so a model that rewrote the state cannot pass /ship quietly.
Done when
A red test: write {"reviewed":true} into state.json directly (any means — sed -i, printf >, python -c) between two plugin reads; the second read reports state.tampered, the state is back to reviewed=false, and the ledger row exists. Before the change the second read trusts the file.
The existing 148-test suite stays green; the text guard is untouched.
A note in packages/guardrails/README.md: the text guard is best-effort; integrity detection is the guarantee; known bypasses of the text guard are listed here, not fixed.
Known bypasses of the text guard (information, not to be fixed one by one)
Codex route A r8 on a8d90ac299: a { inside a word resets the comment state in rawSegments.
Anything the independent acceptance of round 3 lists under "known bypasses".
Activity