Passing threshold: 75%
mcp__crm__write_record to an unscoped pattern so it fires for all tool calls, giving the agent more opportunities to intercept invalid data before a write is attempted.
systemMessage field to the hook's deny response that explains why the contract_id format is invalid and what a valid format looks like, so the agent receives actionable context and stops retrying.
permissionDecision: 'deny' with a post-processing step that silently drops invalid writes after they execute, so the agent receives a success response and does not attempt retries on the same record.
contract_id format validation out of the hook and into the system prompt, instructing Claude to validate all extracted fields before calling mcp__crm__write_record on any contract record.
systemMessage, the agent receives a bare rejection with no explanation of why the operation failed or what to do instead. This leaves the agent in an ambiguous state where retrying the same call is a plausible recovery strategy. Adding systemMessage injects structured context directly into the conversation — naming the violation and specifying the valid format — so the agent understands the constraint and can take corrective action rather than re-issuing the same blocked call. The permissionDecision and systemMessage fields are designed to work together: permissionDecision enforces the constraint deterministically, while systemMessage provides the guidance needed to avoid re-triggering it.
timeout value in the HookMatcher configuration to match the compliance service's latency, and use the AbortSignal from the callback's third argument to handle cancellation cleanly.
PreToolUse hook into the system prompt, instructing the agent to self-validate file paths against compliance rules before issuing any write or edit tool call.
PostToolUse hook that calls the compliance service after each write completes, rolling back any non-compliant file changes detected after execution has already occurred.
timeout value in HookMatcher. The correct fix addresses the root cause directly — raising timeout in the HookMatcher configuration to match the observed latency of the external service, and using AbortSignal from the callback's third argument to clean up gracefully if cancellation does occur. This is the structural mechanism the SDK provides for exactly this failure mode, and it preserves the blocking, pre-execution enforcement that compliance requires.
Specialized Advice category for any comment that initially passes, using a stricter system prompt focused exclusively on financial statements for that second pass.
max_tokens budget for each moderation call so Claude has more room to reason through borderline edge cases before returning a violation verdict on financial statements.
maxTurns value to give each subagent more attempts to resolve the permission issue.
contract-processing Skill and confirm it achieves equivalent performance to each of the three individual Skills it replaces before deploying it to production.
max_tokens limit so they have more room to process the inherited tool definitions alongside the analysis tasks.
/compact on the parent session before spawning subagents so that the summarized conversation history passed to each subagent is shorter.
/mcp disable <name> before spawning subagents, so their tool definitions are not inherited by each subagent.
claude -p call telling Claude to ignore any locally configured hooks or plugins during script execution.
~/.claude directory before running shared scripts so no personal configuration interferes.
claude --bare -p so that auto-discovery of hooks, MCP servers, CLAUDE.md, and plugins is skipped, and only explicitly passed flags take effect.
.mcp.json so every developer loads the same set of servers from the project root.
--bare disables auto-discovery of all environment-specific configuration — hooks, MCP servers, plugins, CLAUDE.md, and auto memory — meaning the script's behavior is determined solely by the flags explicitly passed at the call site. This eliminates the source of divergence: each developer's personal ~/.claude and project-level .mcp.json are never consulted. The fix addresses the root cause structurally rather than relying on developer discipline or instructions to Claude.
max_turns from 30 to 60 so the agent has sufficient headroom to complete the most complex debugging tasks it currently abandons before finishing.
subtype: "error_max_turns", capture the session_id from the ResultMessage and resubmit as a resumption with an additional turn budget, preserving the agent's intermediate progress rather than discarding it.
CLAUDE.md directing the agent to prioritize applying fixes before exhausting its turn budget, so fewer pipeline runs reach the limit and get discarded.
effort from "high" to "low" so the agent uses fewer turns per reasoning step, reducing the probability of hitting the turn limit before completing the fix on complex debugging tasks.
ResultMessage on error_max_turns exposes the session_id precisely to support resumption — the agent loop preserves intermediate context (files read, edits made, test results observed) inside the session. Resuming with the captured ID continues from where the loop stopped rather than restarting cold, which is the mechanism designed for this failure mode. This preserves the safety value of the turn limit (preventing runaway sessions) while recovering from legitimate mid-task interruptions.
max_tokens on the extraction request to ensure Claude has enough room to complete the full JSON parameter before the stream terminates, eliminating truncation at the source.
eager_input_streaming on the extract_claim_fields tool so the API buffers and validates the complete parameter before streaming, eliminating mid-stream truncation at the cost of higher latency.
max_tokens stop reason: wrap the accumulated partial JSON in a structured error object (e.g., {"INVALID_JSON": "<partial>"}) and return it to Claude as an error tool result so Claude can recover with a corrective pass.
input_json_delta event rather than waiting for content_block_stop, so partial fields can be extracted before truncation occurs.
max_tokens is reached — the stream may end mid-parameter. The documented pattern for this failure mode is to detect the max_tokens stop reason, wrap the incomplete fragment in a structured error object ({"INVALID_JSON": "..."}) so Claude can reason about what went wrong, and feed it back as an error tool result for a recovery pass. This addresses the root cause at the point of failure rather than silently skipping documents or removing the capability responsible for latency improvements.
system_warning messages in the conversation history, diluting attention on the extraction schema by the late-session documents.
<budget:token_budget> and <system_warning> updates on remaining capacity — solves exactly this failure mode. Without it, a model "competes without a clock": it has no signal that capacity is nearly exhausted and cannot adjust behavior (prioritizing, checkpointing, or surfacing a graceful handoff) before the window fills. The clustering of failures in late-session documents is the diagnostic signature of a model that doesn't know when it's running out of room. The fix is architectural: use a model with context awareness or design the agent harness to emit explicit token budget signals so the model can adapt before truncation occurs.
description field omits specific trigger terms and use-case context, so Claude cannot reliably distinguish it from the general-purpose Skill when selecting among 120+ candidates.
description field — it is the sole mechanism Claude uses to identify which Skill to invoke. With 120+ Skills, an insufficiently specific description ("Processes data") makes the correct Skill indistinguishable from semantically adjacent Skills. The fix is to include concrete trigger terms and use-case context (e.g., "Extract text, signature blocks, and clause tables from PDF contracts. Use when processing contract PDFs, legal documents, or when the user mentions signatures or clause extraction") so Claude can unambiguously differentiate this Skill at selection time.
PreCompact hook that archives the full transcript before compaction occurs, then injects the archived transcript back into the next session's initial prompt.
/compact before the context window fills, so that developers can time compaction to occur at a natural break point rather than mid-refactor.
allowedTools field is missing or not configured for the MCP servers, so Claude can see the tool definitions but is not permitted to call them.
allowedTools field is a required permission gate in the Claude Agent SDK — it is not inferred from the presence of configured MCP servers. When allowedTools is absent, Claude sees the tool definitions but lacks permission to invoke them, producing exactly the observed symptom: confirmed server connectivity with zero tool calls. The fix is to add allowedTools entries such as "mcp__github__*", "mcp__jira__*", and "mcp__internal-docs__*" (or a wildcard pattern) for each server. This is a structural permission mechanism, not a prompt instruction.
stop_reason before treating a response as complete; when output is truncated mid-tool-use-block, the agent must detect stop_reason: "max_tokens" and treat that invocation as a hard failure requiring retry or escalation — not silently advance to the next item.
stop_reason: "max_tokens" and contains an incomplete tool use block, the tool invocation did not complete — the agent received a partial, unusable result. The correct architectural response is to inspect stop_reason on every API response and treat max_tokens with an incomplete tool use block as an explicit failure signal, not a successful response. Silently advancing is a silent error suppression pattern: the pipeline hides the failure instead of surfacing it. The fix is structural — the agent loop must branch on stop_reason before processing tool call results, then retry or escalate to human review.
max_tokens limit is being reached mid-response, truncating the tool_use block before Claude can close it, producing an unusable partial output that the pipeline treats as a success.
end_turn response as a success when the actual content is empty, masking a prompt construction error.
stop_reason is max_tokens, Claude's response generation was cut short by the token budget — it did not finish naturally. If the truncation occurs inside a tool_use block, the block is structurally incomplete and cannot be parsed or executed. The API call itself succeeds (HTTP 200, no error), so a pipeline that only checks for API-level errors will silently record a success while discarding the unusable partial output. The diagnostic signal (stop_reason: "max_tokens" + incomplete tool_use block) uniquely identifies this cause. The fix is to detect stop_reason: "max_tokens" explicitly and retry with an adequate token budget, not to ignore the stop reason.
--capacity so the server can handle both sessions simultaneously.
claude process alive by pinning it in a background terminal multiplexer.
/remote-control command on VS Code does not support concurrent session features. The fix is to re-launch with claude remote-control server mode and set --spawn=session so a dedicated session is reserved for the developer.
max_tokens to ensure Claude has enough room to process all 400 tool definitions before selecting the appropriate one.
defer mechanism requires Claude Code v2.1.89 or later, and CI environments typically pin to an older version to ensure reproducibility, so the hook result is silently ignored and the tool proceeds through the normal permission flow.
defer is ignored with a warning and the tool proceeds through the normal permission flow, leaving AskUserQuestion undeferred and the headless session hanging as before.
defer mechanism only works in interactive terminal sessions; in non-interactive -p mode the hook result is silently discarded, so the AskUserQuestion call executes immediately and the pipeline hangs waiting for terminal input.
cleanupPeriodDays retention sweep will delete the session before the CI pipeline can resume it, because the default 30-day window does not protect sessions that have been idle for more than a few hours between CI stages.
defer mechanism has an explicit architectural constraint: it only works when Claude makes a single tool call in the turn. If Claude issues multiple parallel tool calls in the same turn, defer is ignored with a warning and the tool proceeds through the normal permission flow. Because the failing runs show AskUserQuestion batched alongside Read calls, returning "defer" from the hook will be silently bypassed, the AskUserQuestion will proceed to execution, and the headless session will hang as before. The fix must address this batching behavior — either by constraining Claude to sequential tool calls or by intercepting AskUserQuestion with a "deny" and injecting a default answer.
tool_choice: {"type": "tool", "name": "run_tests"} to force the first tool call, which is incompatible with extended thinking and silently corrupts the reasoning chain.
budget_tokens value is set below max_tokens, which prevents Claude from completing its reasoning about tool results before generating the summary.
/repo/libs/shared/.claude/hooks/ require CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 to be set before they are discovered from --add-dir paths.
--add-dir directories; --add-dir grants file access only, and hooks are discovered exclusively from the working directory hierarchy and ~/.claude/.
/repo/libs/shared, not when launched from the repo root.
~/.claude/settings.json to fire during file modification; per-project .claude/hooks/ directories are not supported.
--add-dir flag extends the file paths Claude can read and edit — it does not make the added directory a full configuration root. Hooks (along with subagents, commands, output styles, and most other settings) are discovered only from the current working directory and its parents, plus ~/.claude/. Because the team launches Claude from /repo and the hooks live in /repo/libs/shared/.claude/hooks/, those hooks are never discovered regardless of --add-dir. The fix is to either move the hooks to the repo root .claude/hooks/, place them in ~/.claude/ for user-level availability, or launch Claude from /repo/libs/shared when working on that library.
output_config.format parameter conflicts with PDF-type documents specifically; switching the contracts to plain-text document blocks will allow citations and structured outputs to coexist in the same request without a 400 error.
canUseTool callback to explain that /usr/local/lib is off-limits in CI and instruct Claude to use the sandbox path /ci/sandbox/lib instead, so Claude can self-correct on the next attempt.
PermissionResultAllow response with updated_input that rewrites the --target flag from /usr/local/lib to /ci/sandbox/lib before the command executes, so Claude always installs to the correct path without being blocked.
/ci/sandbox/lib as the install target so it never attempts the production path.
canUseTool callback with a PreToolUse hook that auto-approves all pip install commands and relies on post-run test failures to catch bad install paths.
PermissionResultAllow with a modified updated_input that replaces the forbidden path — intercepts the problem at the point of execution rather than creating a feedback loop. Claude sees only the result of the tool execution, not that the input was modified, so it proceeds successfully with the correct sandbox path. This addresses the root cause (wrong path in the command) structurally at the callback layer, eliminating both the denial and the retry loop without requiring Claude to infer the correct path from a message.
timeout value in the HookMatcher configuration to give the compliance API call sufficient time to complete before the hook is considered failed.
PreToolUse hook and implement it as a PostToolUse hook that flags non-compliant results for human review after execution.
timeout field in HookMatcher configuration is the purpose-built mechanism for exactly this failure mode. When a hook callback performs a slow operation — like an outbound compliance API call — the default timeout may be insufficient, causing spurious failures that halt execution. Increasing the timeout preserves the PreToolUse blocking guarantee: the agent cannot proceed until the compliance check returns a decision. This is the proportionate fix that addresses the root cause (insufficient time budget for the callback) without changing the architecture or weakening the enforcement model.
progress.txt narrative, including the names of every test fixed, so Claude can scan the file to determine what has been resolved across sessions.
tests.json) tracking each test's id, name, and status alongside the existing freeform notes, applying each format to what it does best.
id, name, status) gives Claude a machine-readable schema it can query deterministically — checking status: "passing" rather than inferring completion from prose. Combining both formats applies each to the problem it's suited for: structured data for schema-driven state, unstructured text for narrative context.
max_turns to a significantly higher limit so that Claude is far less likely to hit the cap during normal extraction sessions, reducing how often the hook-bypass window can occur in production.
PreToolUse hook matcher from an exact tool name to a wildcard pattern so it fires for a broader set of tool types, making it harder for any single tool call to bypass the validation check.
max_turns, the session terminates before hooks can execute — hooks are session-internal mechanisms and cannot fire after the session boundary closes. A post-session validation gate operates outside the agent session entirely, so it runs regardless of how the session ended. This addresses the root cause: the hook depends on session continuation, which is not guaranteed. Moving validation to a session-external boundary makes it unconditional — the principle of placing enforcement at the layer that is always reachable, not the layer that assumes normal execution flow.
/compact calls at regular intervals so the compactor runs on a predictable schedule rather than automatically, giving Claude a chance to re-read the initial instructions before each compaction.
PreToolUse SDK callback with no matcher, so it fires before any tool call in the session and acts as a session-start proxy.
SessionStart shell command hook in .claude/settings.json and include setting_sources=["project"] in the ClaudeAgentOptions so the SDK loads it.
SessionStart as an SDK callback hook natively.
SessionStart is not available as a Python SDK callback hook — the Python HookEvent type omits it. The correct fix uses the mechanism designed for this gap: define the hook as a shell command in .claude/settings.json and load that settings file via setting_sources=["project"] in ClaudeAgentOptions. This makes the shell command hook available to the Python SDK session without any code restructuring, and is the pattern Anthropic guidance prescribes for this exact capability gap.
/memories and never use relative path components like ../.
/memories directory.
. or ...
pathlib.Path.resolve()) collapses ../ sequences and symlinks into the real filesystem path; checking the result with relative_to('/memories') then definitively rejects anything that escapes the directory. This is a structural enforcement mechanism — it is deterministic regardless of how the path was constructed, including URL-encoded variants like %2e%2e%2f. It addresses the root cause: the tool handler trusts raw input from Claude without validation.
max_tokens budget for the agent so Claude has more room to issue all search calls in a single response turn instead of spreading them across multiple turns.
SearchCodebase as read-only in its tool annotations (readOnly in TypeScript or readOnlyHint in Python) so the SDK can execute multiple instances concurrently rather than sequentially.
SearchCodebase to allowed_tools / allowedTools so the SDK auto-approves each call without waiting for a permission prompt before starting the next one.
SearchCodebase as a single batched tool that accepts an array of search queries so Claude can express all lookups in one tool call.
readOnly / readOnlyHint annotation to determine whether concurrent execution is safe. Marking SearchCodebase as read-only signals to the SDK that parallel execution carries no conflict risk, allowing the 6–8 calls Claude issues per turn to run concurrently instead of serially. This directly addresses the root cause — sequential scheduling of a safe-to-parallelize tool — with the smallest possible change.
irm https://claude.ai/install.ps1 | iex — the Windows-native installer designed for PowerShell.
curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd, since PowerShell supports more syntax than CMD.
'bash' is not recognized error is a definitive signal that a macOS/Linux shell command was run in a Windows environment. The correct fix is to use the PowerShell installer (irm https://claude.ai/install.ps1 | iex), which is the Windows-native installer. This resolves the root cause — shell mismatch — with the smallest change: switching to the correct installer for the correct environment.
claude --continue --fork-session to preserve the original transcript intact and open a new branch with its own context budget for the remaining debugging work.
strict: true only for the tools where schema violations cause real downstream problems, and rely on Claude's natural adherence for the simpler categorization tools.
strict as the first strategy when hitting complexity limits — reserving guaranteed schema validation for tools where violations have real consequences, and relying on Claude's natural adherence for lower-stakes tools. This reduces the total grammar state space without changing the core required fields or restructuring the tools.
--allowedTools "Edit,Bash(git commit *)" to each claude -p invocation to structurally restrict what operations Claude can perform per file.
claude -p to an interactive Claude Code session so the developer can monitor the operations in real time and intervene when Claude goes off-script.
--allowedTools flag enforces a structural permission boundary at the CLI level — Claude cannot invoke tools outside the specified set regardless of what its reasoning suggests. Scoping permissions to Edit and Bash(git commit *) ensures each unattended invocation is limited to editing the target file and committing the result, with no path to remote pushes or config modifications. This is the structural mechanism that makes unattended batch operations safe; prompt instructions (A) are probabilistic and cannot prevent tool misuse when the tools remain available.
enableWeakerNestedSandbox reduces filesystem and network isolation, and allowing /var/run/docker.sock via allowUnixSockets effectively grants the agent host-level access — together these eliminate both the OS-level sandbox boundary and the container boundary simultaneously.
max_tokens so the agent batches more data per request, reducing the number of outbound connections.
enableWeakerNestedSandbox is only a risk on macOS; on Linux the sandbox implementation is strong enough that allowing /var/run/docker.sock poses no additional escalation risk in Docker environments.
enableWeakerNestedSandbox is documented as considerably weakening security — it removes the strong OS-level isolation that is the sandbox's primary defense. Simultaneously, allowing /var/run/docker.sock through allowUnixSockets is explicitly called out as effectively granting access to the host system via the Docker socket. Together, these two choices eliminate independent layers of protection: the first removes the sandbox boundary, and the second grants a direct escalation path to the host. Recognizing that each configuration option weakens a distinct isolation layer — and that compounding them leaves no effective boundary — is the correct architectural judgment.
tools frontmatter field so they can be pre-approved at background launch time.
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 to eliminate background execution and require developers to manually trigger each test suite run.
tools frontmatter ensures the pre-approval prompt at launch covers all needed permissions, eliminating the silent mid-run abandonment without sacrificing concurrency.
get_document_fields tool is overloaded; splitting it into separate tools for each document field type will reduce selection ambiguity and prevent Claude from re-fetching data.
~/.aws/credentials and ~/.kube/config — rather than mounting the full home directory path.
*.pem, *.key, or credentials.
.dockerignore-style filtering to exclude known credential paths (~/.aws/credentials, ~/.kube/config, *.pem, etc.). This eliminates the exposure structurally: files that are never mounted cannot be read, regardless of agent behavior.
paths: frontmatter entirely.
paths: frontmatter so the rule is always loaded. This is the principle of matching persistence mechanism to durability requirement: path-scoped rules trade durability for relevance scoping, and architects must choose deliberately between them.
pause_turn, append the assistant's response to the message history, and issue a continuation request — gating the database write until a non-pause_turn stop reason is received.
max_tokens budget is being exhausted mid-enrichment. Increase max_tokens so Claude has sufficient room to complete the full sequence of lookups in a single response, preventing truncation before all fields are populated.
pause_turn stop reason is returned specifically when the server-side sampling loop reaches its iteration limit (default: 10) while executing server tools. The response at that point contains an incomplete server_tool_use block without a corresponding result. The correct fix is to detect this signal in the agent loop, append the assistant's partial response to the message history, and issue another API request — allowing Claude to resume from where it stopped. Writing to the database before handling pause_turn is what causes the missing fields; the fix is to gate the database write until a non-pause_turn stop reason is received.
stop_hook_active and exit early when the field is true.
type: "http" pointing at a local service will prevent the re-entry loop.
stop_hook_active field is specifically set to true when the Stop event is firing during a hook-triggered continuation. A hook that doesn't inspect this field and exit early will unconditionally trigger another continuation, producing an infinite loop. The fix is to parse stop_hook_active at the top of the script and exit 0 when it is true, allowing Claude to stop rather than re-enter.