@lannguyensi/understanding-gate
v0.7.1
Published
Pre-execution gate that asks AI agents to produce an Understanding Report before acting
Readme
understanding-gate
Pre-execution gate for AI agent harnesses.
Overview
Before an agent edits files, runs destructive commands, or opens PRs, this tool asks it to emit a structured Understanding Report so a human can confirm, correct, or "grill me" before execution begins.
Status: Phase 2 (enforcement) shipped. Phases -1, 0, 0.5, 1, 2 are live: prompt-hook gate, structured report parsing + persistence, and tool-blocking until the report is approved. Phase 3 (agent-tasks lifecycle integration) is next. See ROADMAP.md.
The gate sits in front of your agent harness as two layers, intentionally separated so each does one job well:
Layer 1, the cooperative gate (Phase 0). A UserPromptSubmit hook injects an instruction into every task-like prompt asking the agent to first produce a report covering: current understanding, intended outcome, derived todos, acceptance criteria, assumptions, open questions, out-of-scope, risks, verification plan, prior art (what was searched for an existing solution, the closest match found, and an adopt / extend / build-new judgment). A cooperative agent reads this and pauses for human confirmation. This is where most of the value comes from in practice: the agent slows down on its own and surfaces its interpretation before doing anything irreversible.
Layer 2, the enforced backstop (Phase 2). A PreToolUse hook blocks destructive tools (Write, Edit, MultiEdit, NotebookEdit, Bash on Claude Code; write, edit, bash on opencode) until the latest persisted Understanding Report for the active task has approvalStatus: "approved". Read-only tools (Read, Grep, Glob, LS, ...) stay open at all times. Every block, approve, revoke, and force-bypass lands in .understanding-gate/audit.log. This is what fires when an agent ignores Layer 1, whether because of an aggressive prompt ("don't ask, just do"), a prompt-injection attack, or a less-cooperative model.
Key features
- Two modes for the cooperative layer:
fast_confirm(default, 5-line summary) andgrill_me(10-section report) - Enforced backstop that blocks write/execute tools until a report is approved, with an audited force-bypass and an optional pause sentinel
- One-shot installers for Claude Code and opencode hooks
- Every block, approve, revoke, and force-bypass decision is JSONL-audited
Agentic systems often fail at the transition from partially-understood task to real-world action: the agent infers too much too early, executes on wrong assumptions, and the result is off-target. A pre-execution gate makes the interpretation visible and reviewable before the first impactful action. This is the front-of-pipeline counterpart to claim-gate (no claims without evidence) and review-claim-gate (no merge without checklist). Same family, earlier checkpoint.
Install / quick start
Claude Code
npx @lannguyensi/understanding-gate init --target claude-codeWrites three hook entries into .claude/settings.json (project scope) or ~/.claude/settings.json (--scope user): UserPromptSubmit (Layer-1 prompt template, Phase 0), Stop (parses the agent's final message and persists the report, Phase 1), and PreToolUse (Layer-2 enforcement, Phase 2). All three are installed in one shot. The UserPromptSubmit hook only fires on task-like prompts (keyword classifier), so non-task questions are unaffected. To remove the entries again, run understanding-gate uninstall --target claude-code (respects the same --scope).
opencode (v0.5)
npx @lannguyensi/understanding-gate init --target opencodeopencode has no per-prompt hook before model inference, so v0.5 installs three files: .opencode/rules/understanding-gate.md (the static fast-confirm rule the agent always sees), .opencode/command/grill.md (the explicit /grill command), and .opencode/plugins/understanding-gate-persist-report.ts (a message.updated plugin shim that parses the agent's report and writes it to .understanding-gate/reports/; without this shim Phases 1 and 2 degrade silently). --scope user installs the three files under ~/.config/opencode/ instead of .opencode/. To remove the three files again, run understanding-gate uninstall --target opencode. See opencode integration notes for testing the plugin's failure path.
Non-interactive sessions (claude -p)
Phase 2 works under claude -p as long as the harness ships last_assistant_message in the Stop-hook payload (recent Claude Code releases do). For older harnesses the gate falls back to reading the transcript JSONL, which can race against the harness's flush timing. If .understanding-gate/reports/ stays empty under a -p run while the agent's output clearly contains a # Understanding Report, that race is the most likely cause; upgrade the harness or run interactively as a workaround.
Usage
Modes
| Mode | When | Shape |
|---|---|---|
| fast_confirm (default) | low-risk, small tasks | 5-line summary, "please confirm" |
| grill_me | ambiguous, risky, broad | 10-section report, "please grill me" |
Escalation to grill_me: set UNDERSTANDING_GATE_MODE=grill_me, or include grill me / /grill in the prompt. On Claude Code both modes are persisted: the Stop hook maps the five fast_confirm bullets onto report sections and validates them against a relaxed schema, so a fast_confirm report can be approved like a grill_me report. On opencode, the persist-report plugin only picks up replies that contain an Understanding Report heading line, so use /grill there when Layer 2 needs a report to approve.
Approve / revoke the gate
# After the agent emits a report you accept, in another terminal:
understanding-gate approve # the latest report, when all reports share one taskId
understanding-gate approve --task-id <id>
understanding-gate approve --report-id <taskId|filename|path>
# Reverse it:
understanding-gate revoke
# Inspect:
understanding-gate status # current approval state in cwd
understanding-gate report list # all persisted reports
understanding-gate report show <id> # one report (taskId, filename, or path)The CLI flips the persisted report's approvalStatus field, which is the source of truth the PreToolUse hook reads. Each approve / revoke also drops a JSONL line in .understanding-gate/audit.log (block, approve, revoke, force_bypass, paused_allow, degraded_allow).
approve, revoke, status, report list and report show accept --dir <path> to read reports from another directory. --dir overrides UNDERSTANDING_GATE_REPORT_DIR, which overrides the default .understanding-gate/reports/. revoke takes the same --task-id / --report-id selectors as approve. report list --json prints JSON instead of a table, and status --task-id <id> limits the output to one taskId.
Debugging: understanding-gate print --mode fast_confirm|grill_me prints the snippet the UserPromptSubmit hook injects; --mode full prints the standalone full template, which no hook injects.
Files and state
| Path | Contents |
|---|---|
| .understanding-gate/reports/ | Persisted reports; moved by UNDERSTANDING_GATE_REPORT_DIR |
| .understanding-gate/audit.log | JSONL audit, always under the working directory |
| parse-errors/, sync-errors/ | Breadcrumbs for failed parses and failed hypothesis syncs, next to the reports dir |
| hypotheses.json | Assumptions and open questions registered as hypothesis-tracker entries, next to the reports dir |
The PreToolUse hook checks the latest report for the active task id (UNDERSTANDING_GATE_TASK_ID, else the harness session id), so approving a report for another task id does not unblock this one (sessions that share UNDERSTANDING_GATE_TASK_ID share approval). approve without flags works only when every report shares one taskId; otherwise pass --task-id.
Disable or force-bypass
# Kill switch (gate is off and writes no audit entry of its own; the
# PreToolUse hook still audits a payload that is empty, not a JSON object,
# or has no tool_name as degraded_allow):
UNDERSTANDING_GATE_DISABLE=1 claude
# Bypass enforcement once with a recorded reason (>= 10 chars; logged):
UNDERSTANDING_GATE_FORCE=1 \
UNDERSTANDING_GATE_FORCE_REASON="incident-recovery for ticket 1234" \
claudeFORCE without a FORCE_REASON (or with one shorter than 10 chars) still blocks; the bypass is deliberately friction-bearing.
Documentation
- When does the block actually fire?: what Layer 2 does for a cooperative agent, an aggressive prompt, and a non-cooperative or prompt-injected agent
- Pause sentinel: an optional, read-only mechanism to silence both hook layers on both Claude Code and opencode
- opencode integration notes: testing the
transport_errorbreadcrumb path - Architecture: layered view, harness entry points, lifecycle states, configuration surfaces
- ROADMAP.md: phase status and what's next
Not implemented yet
Phases -1, 0, 0.5, 1, 2 are live. The following items are deliberately out of scope for the current release; some are scheduled for later phases, some are deferred indefinitely:
- Call an LLM for prompt classification. The task-like classifier is a deterministic keyword regex. Determinism plus zero per-prompt latency.
- Sync approval state to
agent-tasks. Phase 3 promotes the local marker to a first-class lifecycle state. - Auto-escalate to
grill_mebased on risk heuristics. Manual escalation only for now. - Time-based expiry of approvals. An approved report stays approved until you revoke.
Development
npm install
npm run build
npm testLicense
MIT, experimental / pre-release. APIs may change between phases.
