# Part 7: How to classify Claude Code governance incidents and manage the limits of hook controls

Canonical page: https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/claude-code-governance-incidents-and-limitations/

Incidents in a Claude Code configuration governed by hooks fall into a small number of patterns: permitted work that a control blocks, a prohibited action refused as intended, a hook error after which the action runs, an alert from a change detector and a refusal by the model in which no control acts. Classify each event by the layer that acted before deciding on a fix, and keep it open until the evidence supports a cause. This part sets out the limits of hook controls, a classification guide built from six representative scenarios, a matrix of control tests and a triage checklist.

Claude Code combines a large language model (LLM) with tools for software work. The controls discussed here are custom hooks, scripts that Claude Code invokes at defined events such as a proposed tool call, used alongside written rules and protections for the files that define the controls. [Part 5: How to govern Claude Code by separating guidance from enforcement](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/claude-code-guidance-and-enforcement/) describes those arrangements.

The [hooks documentation](https://code.claude.com/docs/en/hooks-guide) explains how hook results affect execution, including the treatment of errors that do not block, and the [hooks reference](https://code.claude.com/docs/en/hooks) describes the option that makes a failed hook block the action. The diagnostic procedure referred to below is set out in [Part 6: How to diagnose Claude Code refusals and test governance hooks](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/claude-code-refusals-and-hook-testing/). File names, messages, thresholds and model identifiers in this part are examples; adapt them to your own configuration and confirm current behaviour against the documentation.

## Terms used in this part

| Term | Meaning in this part |
|---|---|
| False positive | A control blocks an action that the intended rule should allow. |
| Crash that fails open | A control fails and the action proceeds. |
| Guidance refusal | The model declines an action because of instructions or remembered context, without requesting a tool. |
| 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. |
| Tripwire | A check that compares protected configuration files with a recorded baseline and flags any difference for review. |
| Shared configuration | The files at user level that apply to every project, such as the user CLAUDE.md, settings, hooks and agent definitions. |
| Configuration owner | The person or role accountable for the shared configuration and authorised to change it. |
| Decision record | A file that records each configuration decision with its status, reason and consequences. |
| System guard | A hook that refuses manipulation of operating system services and writes inside the SSH key directory. |
| Shell guard | A hook that bounds and routes shell commands and refuses commands other than reads on shared configuration paths. |
| Immutable attribute | A file system flag that prevents any change to a file until a privileged user removes it. |
| Synthetic payload | Test input, in the format Claude Code sends, supplied directly to a hook script. |
| Traceback | An error record showing where program execution failed. |
| Subagent | An agent assigned a delegated task with its own working context. |
| Compaction | Condensing accumulated conversation into a summary to support continued work. |
| SSH | Secure Shell, a protocol for remote access whose key and configuration files a guard should protect. |
| Git | The version control system used to record and inspect file changes. |

## Known limits of hook controls

Under auto mode, in which a classifier rather than a person approves most tool calls, hooks may be the only deterministic stop. They inspect requests rather than effects, and the evidence they leave has gaps. Track each limit below as a residual risk with a compensating control.

| Limit | Consequence | Compensating control or action |
|---|---|---|
| A hook that matches patterns sees the command string and the tool arguments, not what a script does once it runs | A program that opens a protected path from inside a file is a route the hooks cannot close | Protect critical files with the immutable attribute and monitor them with a tripwire, in addition to the hooks |
| Some hook input fields, such as those for subagents and the prompt field of the Agent tool that launches them, may be confirmed only by live use, not by documentation | A guard that relies on them rests on observed behaviour rather than a documented interface | Record the dependency, deny when an expected field is absent and test the guard again after each Claude Code upgrade |
| The transcript may show only the first line of a hook's error output | A crash whose traceback is not captured cannot be diagnosed and remains unresolved | Capture the full output with debug logging (`claude --debug`, or `/debug` during a session) and keep the incident open until the traceback is available |
| A tripwire that hashes the raw bytes of a file fires whenever the runtime rewrites it, for example by reordering keys | An alert that fires on every start will be ignored | Hash canonical content with the keys the runtime manages removed, and read the git diff before trusting any alert |
| The model setting is an initial selection, not enforcement ([model configuration documentation](https://code.claude.com/docs/en/model-config)), and a saved model choice can change the default | A session can run on an unintended model without anyone noticing | Print the resolved main model at every session start, and use a managed allowlist where the choice must be enforced |
| Routing rules, which send work such as test runs to a subagent on a smaller, less expensive model, may meet legitimate exceptions | An exception granted within a session weakens the control without a record | Treat each exception, such as allowing a single named test file to run in the main session rather than in a subagent, as a decision for the configuration owner, recorded in the decision record |

![A hook inspects command strings and tool arguments but not a program's internal file actions. Separate status boxes show fields confirmed only in live use, an unresolved crash, needed tripwire work, pending model printing and an exception left to the owner's decision.](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/figures/fig-07-01-inspection-boundaries-and-open-items-remain-visible.svg)

*Figure 7.1. The inspection boundary of a hook that matches commands, set apart from the evidence gaps and open items to track: fields confirmed only in live use, a crash without a captured traceback, tripwire hardening, printing the model at session start and an exception reserved for the owner's decision.*

## Incident classification guide

Classify every event by the layer that acted, using the tests in Part 6, before deciding on a fix. The six scenarios below are representative patterns. The table gives the classification; the subsections give the likely cause and the corrective action, and scenarios 2 and 5 are worked through step by step.

| Scenario | What is observed | Layer that acted | Classification |
|---|---|---|---|
| 1 | The tripwire flags a change to `settings.json` | Detection | Change to protected configuration |
| 2 | A command that only reads a protected file is refused | Hook (system guard) | False positive |
| 3 | A prohibited operation is refused and a related harmless one is allowed | Hook (system guard) | Correct refusal and correct allowance |
| 4 | A delegated agent's compound command is refused as a write to the shared configuration | Hook (shell guard) | False positive |
| 5 | A hook error notice appears and the command runs | Hook crash that fails open | Control failure |
| 6 | The model declines test commands and no hook runs | Guidance | Guidance refusal |

### Scenario 1: the tripwire flags a change to the settings file

The tripwire reports that `settings.json` has changed. Treat this as a detection event and read the git diff before deciding whether the change was authorised. Claude Code can write to the file itself: keys may be reordered at startup, and a model choice saved from another session can change the default model, for example to the least expensive tier. Set the default model through the `ANTHROPIC_MODEL` environment variable, which takes precedence over the model key in any settings file ([settings documentation](https://code.claude.com/docs/en/settings)), and make the tripwire compare canonical content so that a reordering alone raises no alert.

### Scenario 2: a command that only reads a protected file is refused

1. A command that only reads the SSH configuration is refused.
2. Check the breach log. A line at the time of the refusal shows that a hook acted. If the line does not record the command, change the guard so that every refusal logs it; without the command, the cause can only be guessed.
3. Reproduce the refusal by passing the logged command to the guard as a synthetic payload. A deny decision confirms the hook's behaviour independently of any session's memory.
4. Inspect the guard. In this pattern, the guard applied its write test to the whole command, so a redirect elsewhere in the command was enough to trigger a refusal.
5. Correct the guard so that it tests only the write target, then reproduce again. The lookup should now be allowed, and a write inside the SSH key directory should still be refused.
6. Accept a diagnosis only when reproduction confirms it. Explanations formed from recollection, before the command was logged, can be wrong.

### Scenario 3: a prohibited operation is refused and a related one allowed

A guard refuses the creation of an operating system service and allows a service query. The control is working as designed, and the first such event in live operation is useful evidence that the control is active. Record it with its scope: it is evidence about those two operations and supplies no rate of correct decisions.

### Scenario 4: a compound command is refused as a write to the shared configuration

A delegated agent issues a compound command, several commands joined on one line, to produce a report, and the shell guard refuses it as a write to the shared configuration. This is a false positive of the same class as scenario 2, on shared configuration paths: the guard evaluated the compound command as one unit. Make the guard evaluate each segment separately, review how it treats git write operations on the configuration repository, and widen the portion of each command that the breach log records.

### Scenario 5: a hook error appears and the command runs

1. The transcript shows `Hook error ... non-blocking status code`, and the command ran.
2. Classify the event as a crash that fails open. By default this is documented behaviour: a hook that exits with an unexpected code is treated as an error that does not block, and the action proceeds.
3. Capture the traceback at once. If the transcript shows only the first line of the error, enable debug logging and reproduce the failure.
4. Consider where the failure occurred. If the hook's main function already denies on any exception, an error that still let the action through probably occurred outside that wrapper, at import time. Without the traceback, this remains a probable explanation, not a confirmed cause.
5. Treat the event as a defect in the hook, whatever the command was. Add a crash test at import time, declare failure blocking where the Claude Code version supports it, and keep the incident open until the traceback is captured.

![One incident path follows a refused SSH lookup through log-supported reproduction to a target-aware correction. A separate fail-open event has no captured traceback, leaving its cause unresolved; a dashed block marks an import-time exception as an unconfirmed explanation.](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/figures/fig-07-02-recorded-evidence-supports-different-incident-conclusions.svg)

*Figure 7.2. Scenarios 2 and 5. The refused lookup is traced through the logged command and direct reproduction to a correction that tests only the write target. The crash that fails open has no captured traceback and remains unresolved; an exception at import time is a probable explanation, not a confirmed cause.*

### Scenario 6: the model declines and no hook runs

The model declines several test commands, and no hook is involved: there is no breach log line, and the transcript shows prose with no tool call. The model applied the rules before any hook ran. The behaviour is correct, but it demonstrates guidance, not enforcement. Test the hook directly with a synthetic payload, as Part 6 describes, before relying on it.

## Control test matrix

Run these tests when a control is installed, after every change to it and after each Claude Code upgrade. Each row is derived from one of the scenarios above; extend the matrix for every guard you add.

| Control | Test | Expected result | Evidence |
|---|---|---|---|
| System guard | Request a prohibited operation, such as creating an operating system service | Refused | The hook's text in the tool result and a breach log line |
| System guard | Request a related operation that changes nothing, such as a service query or a lookup that only reads the SSH configuration | Allowed | The tool runs and no breach log line is written |
| System guard, write test | Read a protected path and redirect the output to an unprotected path | Allowed | No breach log line |
| System guard, write test | Write inside the SSH key directory | Refused | A breach log line naming the rule |
| Shell guard | Run a compound command whose segments only read shared configuration paths | Allowed | No breach log line |
| Shell guard | Run a compound command in which one segment writes to a shared configuration path | Refused | A breach log line naming the rule |
| Crash handling | Raise an exception inside the hook's main function | Refused | A deny decision from the synthetic payload |
| Crash handling | Raise an exception at import time, or point the hook at a missing script | Refused where failure blocking is declared; otherwise the action proceeds and the gap must be recorded | The transcript notice and the debug log |
| Tripwire | Start a session without changing protected content | No alert | The status line |
| Tripwire | Change the content of a protected file | Alert | The status line and the git diff |
| Model check | Start a session after a model choice has been saved in another session | The resolved model is displayed and matches policy | The session start output or the status line |

## Triage checklist for refusals and routine checks

1. Look in the breach log for a line at the time of the refusal. A line means a hook refused; no line means the model declined.
2. Look at the transcript. A tool result carrying the hook's text means a hook; prose with no tool call means the model.
3. Ask for the command exactly, once, as a hook test. If there is still no tool call, the refusal is guidance.
4. Reproduce directly: pipe a synthetic payload into the hook script and read its decision.
5. After a resume or a compaction, state that the rule has changed and ask again; a remembered refusal can be stale.
6. If `Hook error ... non-blocking status code` appears and the action ran, the hook crashed and failed open. Capture the traceback.
7. After any change to hooks or settings, restart the session. At every session start, check the model.
8. Fix a false positive by narrowing the pattern to the segment or target, never by widening an allow list in the session.
9. Read the breach log weekly. Constant firing indicates a wrong or ignored rule; silence calls for a forced test.

## Principles for investigators

- Preserve the command and the control decision with every refusal, so that an explanation can be tested rather than recalled. In scenario 2, the logged command and direct reproduction identify the cause.
- Do not close an incident on a probable cause. In scenario 5, an exception at import time remains a probable explanation until the traceback is captured, and the incident stays open.
- Treat worked scenarios as diagnostic cases, not as rates. A success or failure rate needs a defined population of permitted and prohibited cases; [Part 8: How to evaluate an LLM governance system](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/evaluate-llm-governance/) explains how to define one.
- Read a guidance refusal narrowly. It shows how the model applied the rules in that case, not that enforcement works; the direct reproduction test in Part 6 examines the hook's decision independently of the model's memory.

## Basis of this guidance

Statements in Parts 5 to 7 about hook exit codes and failure handling, context injection events, CLAUDE.md delivery and reloading, the recommendation to keep CLAUDE.md under 200 lines, the arbitrary resolution of conflicting instructions, the precedence of the model setting, managed settings precedence and the merging of plugin settings across scopes rest on the Claude Code documentation as read in October 2026. The scenarios, token figures, file sizes and thresholds come from the operation of a working configuration; they illustrate how to classify and test events and do not supply general rates. The claims in Parts 5 to 7 about degradation with context length, the cost argument for compaction at task boundaries, the cost argument for writing documents in the main model and the recommended review cadence are judgement.

Part 7 of 8 in the series [LLM harnesses and governance](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/).

Previous part: [Part 6: How to diagnose Claude Code refusals and test governance hooks](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/claude-code-refusals-and-hook-testing/)

Next part: [Part 8: How to evaluate an LLM governance system](https://vikrammohan.com/thought-leadership/llm-harnesses-and-governance/evaluate-llm-governance/)

Vikram Mohan is not a solicitor or barrister. Nothing on this site is legal advice. For legal advice, speak to a regulated legal professional.

© Vikram Mohan - All rights reserved. No liability accepted for content on this website. [LinkedIn](https://www.linkedin.com/in/mohanvikram/) · [Privacy](https://vikrammohan.com/privacy)
