In retrospect, it was pretty obvious... sharing it anyway.
I'm currently working on a self-steering progressive disclosure system, and I wanted my agent to read a design doc before it touched the code that doc governs. Simple PreToolUse hook: match Edit/Write, check whether the doc was Read this session, and if not, stop the edit and tell the agent to read it first.
I returned permissionDecision: "ask". My reasoning at that time: "ask has an escape hatch. Approve and the call proceeds, so a bad condition can never wedge the agent - you can always wave it through."
That safety was the whole problem. The approval is an escape from the prerequisite, not a path through it. The edits went through without the doc reads.
Here is what ask actually does. It surfaces the tool call for approval. Approve it - or auto-approve it - and the action runs. The doc never gets read. ask is a checkpoint for a human, not a gate on the agent. Nothing about it makes the agent do the prerequisite step.
The fix was one field. Return deny:
// PreToolUse hook, matcher: Edit|Write
if (!wasReadThisSession(governingDoc)) {
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: `Read ${governingDoc} before editing this, then retry.`
}
};
}
deny refuses the tool call and hands the agent the reason. The agent can't proceed, so it does what the reason says - reads the file - and retries. The read clears the check, the retry passes.
This is the behavior Claude Code already ships natively: when it tries to Edit a file that haven't been Read before, the tool refuses the action until the relevant Read happens. The deny hook lets you apply that same read-before-edit rule to any file you designate, not only the file being edited.
The distinction I missed:
askgates on approving the call.denygates on the agent doing the prerequisite work first.
If the goal is to force the agent's own behavior - read this, run that check, load that context - deny is the lever. ask just adds a human to the loop.
Important: this only works, if the gate releases accurately, when the prerequisite is actually done. A deny on a condition that never clears will wedge the agent. Mine keys off "was this file read this session," which the read itself flips.
Top comments (0)