#2888·gstack

Opt-in dual-engine guard for /careful — deterministic rules first, LLM intent audit second, with a content-bound audit cache

Author: sky-mirrorsCreated Sep 17, 2026Updated Sep 17, 2026

Problem

/careful today is a pure pattern matcher (careful/bin/check-careful.sh): a MEDIUM tier of destructive families that is "always overridable", and a HIGH tier that is itself documented as "a best-effort advisory hard-stop, not a policy boundary". That design is right for what it does — pattern lists are free, deterministic, and immune to prompt injection.

But a pattern list is an enumeration of known bad shapes. It cannot catch the failure mode where the command is a perfectly ordinary shape and still wrong:

  • Intent mismatch: the user said "back up the database" and the model runs the drop command it used last session.
  • Target mismatch: the right command aimed at the wrong file — rm -rf src/ when the conversation was about dist/.
  • Parameter hallucination: a plausible-looking flag that changes semantics (--force vs --force-with-lease is the classic).

These are semantic failures. No regex enumerates them, and in spawned/auto-choosing sessions (the same class of context /ship runs its doc-sync in) there is no human mid-run to notice. The gap is structural: gstack's strongest safety tool checks what the command is, never what the command is for.

Proposal

An opt-in second engine for /careful, layered so the existing one is untouched:

Engine order is load-bearing

  1. Rules layer stays first and short-circuits. If the pattern matcher hard-denies, the LLM is never consulted — zero tokens, zero latency. This is also a security property, not just an economy one: an LLM judge can be prompt-injected by file content it reads, so the deterministic layer must remain the first arbiter. The LLM only ever adds friction; it can never subtract the rules layer's verdict.
  2. For write-class commands that pass the rules, an LLM audit answers exactly three questions, forced-JSON:
    • intent_match — does this command match the user's stated intent in this conversation?
    • parameter_sane — are the arguments plausible for the named target?
    • risk_level — low / medium / high.

Triple fail-closed

Any failure in the audit path blocks: JSON parse failure → block; API call error → block; confirmation channel broken → block. This is the same fail-closed family as the egress receipt auditor and verify-gate — a guard whose failure mode is silence is worse than no guard.

The audit cache — why both key components are necessary

Audits cost tokens, so verdicts get cached. The cache key must be:

command + hash(intent, first 500 chars of the user's request) + content hash of target source files

Each component exists because omitting it is a correctness bug, not a performance trade-off:

  • Intent fingerprint. Without it, "delete it" and "back it up" against the same file share a cache key — the second request inherits the first request's verdict. An intent-sensitive guard whose cache is intent-blind is broken by construction.
  • Target content hash. Without it, a verdict survives the file changing underneath it. "This destructive edit on config.yaml is safe" was computed against yesterday's config.yaml. gstack already established this exact principle for reviews: gstack-wtree "binds reviews and test evidence to content instead of commit SHAs". The audit verdict is the same kind of artifact and gets the same treatment — bound to the (command, intent, target content) triple.

gstack has been burned by cache pollution before: the gbrain .env incident (PR #1583) had one poisoned file "poisoning every probe from anywhere" through a shared 60s state cache, and the slug-sanitize and ios-qa accessor tests both encode cache-key-injection lessons. PR #2858 ("prevent gstack-slug cache poisoning from env-var leaks and $HOME cloning", open at time of writing) is the newest member of the family: an under-keyed slug cache letting a project's reviews, learnings, and decisions silently split across an unrelated namespace.

Cache parameters: 24h TTL, 500 entries, evict oldest by timestamp. Per-project, under the state root like every other gstack cache — never shared across projects or users.

Prior art, and how this differs from #1091

#1091 proposed enriching /careful and /guard with an external nine-question protocol plus durable execution records, and was closed as not_planned — defensibly, on complexity, privacy exposure, and the absence of an agreed execution contract. This proposal deliberately takes the opposite shape on every axis that got #1091 closed: no external framework, no durable records beyond a per-project verdict cache, a three-field JSON contract instead of nine questions, default off with no setup registration, and an audit layer that is strictly additive to the existing rules layer rather than a replacement for it. The structural question #1091 pointed at — "does the AI actually have enough information to judge whether this action is safe to execute right now?" — is real. The answer here is minimal and incremental rather than protocol-sized.

How this fits gstack

  • AI models recommend. Users decide.intent_match is the execution-side guard of exactly this rule. The pattern layer checks the letter of the command; the audit checks whether the model is executing the user's decision rather than something adjacent to it.
  • Fail-closed precedent — egress receipts, verify-gate's blocked-turn semantics, issue-guard's tracker-text trust envelope. The audit engine joins that family; nothing about it may fail open.
  • User sovereignty in adoption — the engine is off by default and per-project opt-in, mirroring how verify-gate is opt-in (./setup never registers it for you). Users who want pure-static /careful keep it, byte for byte.

Non-goals / open questions

  • No changes to the existing MEDIUM/HIGH tiers or their patterns.
  • Default off; no setup registration.
  • Open: whether risk_level: high should downgrade to an interactive ask even when intent_match is true (destructive-but-intended commands); whether the audit should also see the last N tool results, or only the request text (smaller prompt, less injection surface — leaning toward request text only).

Suggested TODOS.md entry

What: Add an opt-in LLM intent-audit engine to /careful (three-question JSON audit after the rules layer, triple fail-closed) with a content-bound audit cache keyed on command + intent fingerprint + target content hash.

Why: The pattern matcher enumerates known bad command shapes but cannot catch semantic failures — intent mismatch, wrong target, hallucinated flags — and spawned sessions have no human mid-run to notice; gstack's own cache-pollution history (PR #1583) shows what an under-keyed cache does to a guard's correctness.

Context: Rules layer always first (deterministic short-circuit; LLM can be injected, so it may only add friction). Cache key correctness: intent hash prevents verdict-sharing across different requests for the same file; content hash binds verdicts to target state per the wtree content-not-SHA principle. 24h TTL, 500 entries, per-project state root. Fail-closed on parse/API/confirmation failures.

Effort: M Priority: P2 Depends on: None