When Claude Code does not run a command, either the model chose not to request it or a control refused the request. Identify which layer made that decision before changing anything, because the remedies differ. A hook refusal leaves a tool result and, where hooks log their refusals, a line in the breach log, the file to which each logging hook adds one line per refusal. A guidance refusal is the model declining to request the action, and it leaves only prose. This part sets out six diagnostic tests, a forced failure test for every hook and the routine checks that keep the controls trustworthy.
Claude Code is an application that combines a large language model (LLM) with tools for software work. Its operating harness, the software around the model, coordinates the model and those tools. A hook is code that Claude Code invokes at a defined event, such as a proposed tool call. The procedure assumes controls of the kind recommended in Part 5: How to govern Claude Code by separating guidance from enforcement: enforcement hooks that deny on error and log every refusal, alongside written rules that guide the model.
Terms used in the diagnostic procedure
| Term | Meaning |
|---|---|
| Guidance refusal | The model declines an action because of instructions or remembered context, without requesting a tool. |
| Hook refusal | A hook decision prevents a requested tool call from proceeding. |
| Breach log | A log file to which each enforcement hook adds one line whenever it refuses an action. An entry records a refused attempt, not a successful breach. |
| Fail closed | An operation is withheld when the relevant control cannot evaluate it. |
| Fail open | Execution continues when the relevant control cannot evaluate it. |
| False positive | A control blocks an action that the intended rule should allow. |
| Synthetic payload | Test input, in the format Claude Code sends, supplied directly to a hook script. |
| Transcript | The recorded conversation, including model responses, tool requests and tool results. |
| Traceback | The error record showing where program execution failed. |
| Compaction | Condensing accumulated conversation into a summary to support continued work. |
| Auto mode | A permission mode in which a classifier, rather than a person, approves or refuses most tool calls. |
| Shared configuration | The files at user level that apply to every project, such as the user CLAUDE.md, settings, hooks and agent definitions. |
| Tripwire | A check that flags changes to the shared configuration by comparing it with a recorded baseline. |
| Decision record | A file that records each configuration decision with its status, reason and consequences. |
| Immutable attribute | A file system flag that prevents any change to a file until a privileged user removes it. |
| Main model | The model that runs the main session, makes the decisions and judges delegated work. |
| Subagent | A delegated agent given a task and its own working context. |
| Less costly agent | A subagent that runs on a smaller, less expensive model and executes bounded jobs. |
| Git | The version control system used to record and inspect file changes. |
What the separation gives an organisation, and how to confirm it
Separating guidance from enforcement changes cost, exposure, safety and the quality of evidence. Confirm each benefit in your own environment rather than assuming it.
| Benefit | What changes | How to confirm it |
|---|---|---|
| Lower context cost | Removing a reminder that reprints the rule headings on every turn, and shortening the universal rules file (for example, to about a quarter of the previous combined load), saves tokens on every turn, so the saving compounds over a session of hundreds of turns. Tool output, which can make up almost half of a long session's context (for example, 364,500 of 780,000 tokens), is bounded by the shell guard and the read cap, and compaction at task boundaries keeps the context near the compaction trigger rather than near the limit of the context window. | Compare context composition and cost for comparable sessions before and after the change. |
| Better adherence | The documentation links adherence to the length of CLAUDE.md and recommends keeping each file under 200 lines (CLAUDE.md documentation). | Count the lines that the user CLAUDE.md loads, including imports. |
| Less exposure | Names, host addresses and project conventions no longer enter sessions that have nothing to do with them, nor the prompts of subagents launched from those sessions. | Inspect what a new session in an unrelated project loads. |
| Safety under auto mode | The only deterministic stops deny when they cannot evaluate. The model cannot change the files that define them through any route the hooks see; a change through a route they cannot see is flagged by the tripwire and can be reversed from git; and a disallowed model cannot be reached by alias. | Run the forced failure tests below and a tripwire test. |
| Cost control | The less costly agent does the work whose output is large and whose acceptance is mechanical. Its briefs, the written instructions for each job, are capped, and its context is kept below any size at which the provider's price per token rises. The main model is reserved for decisions. | Review the agent launch log and the cost per accepted task. |
| Reproducibility | A refusal logs the command, so a reported false positive can be reproduced against the hook in one line. The configuration is under version control, and the decision record states what is operative. | Reproduce a sample of logged refusals. |
| Recoverability | Every installation of a hook and every fix is backed up beside the file and centrally; git holds every version of the configuration; and the immutable attribute prevents accidental overwriting and makes deliberate change a visible act. | Restore a previous version in a test environment. |
Identify which layer refused the command
Answer this question before anything else, because the two layers have different remedies. A hook false positive is fixed by narrowing a pattern; a guidance refusal is addressed by a clearer instruction or by telling the model that the rule has changed. Apply the six tests in the order given.
Test 1: Check the breach log
A hook refusal in a logging configuration writes one line to the breach log, for example ~/.claude/state/breach.log, with the timestamp, the session identifier, the rule tag and the command (in the example configuration, its first 600 characters). A model refusal writes nothing. Run the following and look for a line at the time of the refusal:
grep "$(date +%F)" ~/.claude/state/breach.log | tail
Test 2: Inspect the tool result and the assistant response
A hook refusal appears as a tool result carrying the hook's own text, such as BLOCKED by scope_guard or a warning that a path belongs to the shared configuration. A model refusal is prose in the assistant turn, with no tool call shown. In the terminal, a tool call that ran shows a line such as Ran 1 shell command above the reply; a refusal without such a line was the model's.
Test 3: Request the exact command once
Ask for the command verbatim, prefixed with this is a hook test; run it exactly. If the model still declines without making a tool call, the refusal is guidance. Insist once for a test and no more, because the model's refusal may be the correct reading of a rule that the organisation also holds.
Test 4: Reproduce the decision directly against the hook
Feed the same command to the hook as a synthetic payload and read its decision:
echo '{"tool_name":"Bash","tool_input":{"command":"<the command>"}}' | python3 ~/.claude/hooks/<hook>.py
A deny decision on standard output means that the hook refuses the command; empty output means that it allows it. If a hook signals refusal by exit code instead, check the exit status as well: code 2 means refusal. This is the only test that is independent of what any session remembers, which makes it decisive when other evidence conflicts.
Test 5: Check for a remembered refusal after resuming
After a resume or a compaction, the model may carry a refusal from before a hook was changed. State that the rule changed at a given time and that the earlier refusal no longer applies, then ask again. If the command now runs, the earlier refusal was stale guidance.
Test 6: Identify a crash that allowed execution to continue
The harness message hook error ... Failed with non-blocking status code followed by the action proceeding means that the hook crashed and did not deny. Capture the traceback at once and treat the event as a defect in the hook, whatever the command was. The transcript may show only the first line of the hook's error output; the hooks documentation describes debug logging, started with claude --debug or with /debug during a session, which captures the full output.
Test every hook by making it fail
Test every hook in three ways: with a command it must refuse, a command it must allow and a crash. The crash test is the one most often skipped and the one that matters most under auto mode. An exception raised inside the hook's main function must produce a deny. An exception raised at import time, above any wrapper, will not, so place a crash test where a real failure would occur, not only at the top of the file.
Where the Claude Code version supports it, also declare "onFailure": "block" on each safety hook and add a test in which the hook cannot start, for example because its script path is wrong. The hooks reference describes this test and the conditions under which a failure then blocks the action. Keep the wrapper that denies on error as well, because the option does not apply to every event or version.
| Case | Example input | Expected result |
|---|---|---|
| Must refuse | A command in the protected class | A deny decision, and a breach log line in live use |
| Must allow | A similar command outside the protected class, such as a read of the same path | Empty output and exit code 0 |
| Crash inside the main function | A malformed payload, or an exception raised inside the main function | A deny decision |
| Crash at import time | An exception raised above the wrapper | No deny from the script; refused only where failure blocking is declared |
| Hook cannot start | A wrong script path | Refused only where failure blocking is declared |
Record the result of every case whenever a hook changes, and run all five again after each Claude Code upgrade.
Routine checks after changes and during operation
| Trigger | Action | Note |
|---|---|---|
| Any change to hooks or settings | Restart every session, then run the forced failure tests for any changed hook | Hook configuration loads when a session starts. A file watcher normally picks up direct edits, but a session that has not picked up a change keeps the old behaviour (hooks documentation). |
| Every session start | Check the resolved main model, on the status line (the line of session information that Claude Code can display) or with the model command | The model setting is an initial selection, not enforcement (model configuration documentation). A model choice saved from another session can change the default, for example to the least expensive tier, and the change can go unnoticed unless the model is displayed. |
| A tripwire alert | Read the difference with git before trusting the alert | A hash of the raw bytes fires when keys are reordered at startup, and an alert that fires on every start will be ignored. |
| A hook refuses legitimate work | Log the refusal, reproduce it directly and narrow the pattern to the segment or target that names the protected path | Never widen an allow list in the session that hit the refusal. |
| Weekly | Review the breach log | A rule that fires constantly is either wrong or being ignored; a rule that never fires needs its forced test run again to confirm that it still works. |
Record what each diagnosis established
For each refusal or hook error, record the layer that acted, the evidence that identified it (a breach log line, a tool result, a reproduction or a traceback), the fix and anything left open. An event without decisive evidence remains open. Part 7: How to classify Claude Code governance incidents and manage the limits of hook controls gives a classification guide for common scenarios and a matrix of control tests.