Rule ids on the gate's rules, recorded on every denial as
{"rule":"<id>","payload":<hook event>} and read back by
vm_tool_gate::denial_detail into gate.denied events, so 'which rule fires
and how often' is a query on detail->>'rule'. Older bare-payload lines
still parse, with no rule.
A write-path policy: Write/Edit/MultiEdit/NotebookEdit are refused when
aimed at the hooks, their records, or the settings that install them, on
either tier (/root/toolgate, /root/toolhooks, /root/tap, the guest and
.claude settings, and .git/hooks/ — a hook in the checkout runs with the
agent's authority unseen). An agent could Write over denied.jsonl and erase
its own denials; now it cannot, and the same paths are refused to Bash
whatever the tool in front of them (sed -i is a text tool that writes).
The extractor prints file_path as a third line; the predicate and the
generated shell are tested against the same payloads, and the shell test
now reads the record back.
The container tier drained the tap and the inert marker and never the
denials: gate.denied existed only for microVM phases. drain_denied fixes
that. Harness: a gatepolicy scenario with two negative controls on the
container tier, asserting the two rule ids in the record.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz