Study guides / CCAR-P / Domain 7

Developer Productivity & Operational Enablement · Lesson 3 of 3

7.3 - Debugging and Operational Issue Resolution

Isolate which layer of a Claude-based system or Claude Code environment failed, use the right diagnostic and request identifiers, and decide when and how to escalate to Anthropic or your own platform team.

A wrong answer, a hung pipeline, an ignored instruction and a 4xx all present as “Claude is misbehaving”, yet they live in different layers with different fixes. Debugging at this level is layer isolation: prove which layer failed before you change anything in it, and change one variable at a time. The most common wrong move, and a favourite exam distractor, is to fix the layer you can see (the prompt) instead of the layer that failed (what loaded, what was sent, who rejected it).

The layers, and the first diagnostic for each

SymptomLayerFirst diagnostic
Instruction ignored, hook silent, MCP tools or skill missingConfiguration loading/context (memory files, MCP tools, skills), /status (setting sources), /permissions, /hooks, /mcp
File loaded but rule not followedInstruction qualityLength, vagueness, conflicting files; if it must hold, enforce with a permission rule or hook
Works for one developer, not anotherLocal environment driftclaude doctor or /doctor; claude --safe-mode; clean config directory
5xx, 529, 429, login or proxy errorsProvider, account, networkThe Claude Code error reference, the status page, usage and spend limits
4xx from your own integrationRequest constructionerror.type, message and request_id from the response
Clean transport, poor contentModel behaviour and promptMinimal repro, examples, an evaluation set

Only the last row is a prompt problem. The loading row hides classic causes: a subdirectory CLAUDE.md loads when Claude reads a file there, not when it creates one; Explore and Plan subagents skip CLAUDE.md; a hook matcher written as a JSON array is a schema error that rejects the file; hooks in a standalone file and .mcp.json inside .claude/ are never read; a project MCP server stays off until approved; and Bash(rm *) does not match /bin/rm.

Reproduce, then bisect

Shrink the failure to the smallest input and configuration that still fails, and record the prompt, model, Claude Code version and settings sources. Output varies, so rerun before concluding.

To bisect configuration, launch claude --safe-mode, which disables CLAUDE.md, skills, plugins, hooks, MCP servers and custom commands and agents while leaving authentication, models, built-in tools and managed policy in place. If the problem vanishes, reintroduce one surface at a time. If it persists, point CLAUDE_CONFIG_DIR at an empty directory and start from a folder with no .claude, .mcp.json or CLAUDE.md; managed settings still apply, so read /status. To watch behaviour live use claude --debug (optionally a category such as --debug='mcp,startup') or --debug-file <path>; the hook debug log records which matchers were checked and each exit code, and MCP server stderr appears in the debug log under ~/.claude/debug/.

Non-interactive runs need extra care because a -p run shows no dialogs: it skips broken settings and continues, so run claude doctor to see what it dropped. With --output-format stream-json the system/init event lists mcp_servers, mcp_server_errors, plugins and plugin_errors, so a CI gate can fail on a non-empty error array, and system/api_retry events report the failing status and error category of each retry.

Reading API errors and request IDs

Every API error is JSON with a top-level error object holding a type and message, plus a request_id; the same value arrives in the request-id response header on every response, successful or not. Read status, error.type and message together, and log the ID on every failure.

The SDKs raise typed exceptions (catch the most specific class first, never string-match) and automatically retry connection errors, 408, 409, 429 and 5xx with backoff, twice by default, honouring retry-after. Do not stack a second retry loop on top without disabling one, and do not resend a deterministic 4xx unchanged. A stream can fail after a 200 with an SSE error event, so stream handlers need their own error path.

The Claude Code error reference groups messages into server errors, automatic retries, usage limits, authentication, network and request errors (prompt too long, tool schemas, model access). With OpenTelemetry enabled (CLAUDE_CODE_ENABLE_TELEMETRY=1) the claude_code.api_error event carries the Anthropic request_id from the response header, plus a client-generated client_request_id that exists even when a timeout or connection failure never produced a server ID, and session.id and prompt.id correlate events. Prompt text is not logged unless you opt in.

The request ID is your anchor

Capture it in application logs at the moment of failure. Without it you cannot match a user report to a server-side event, and support cannot investigate a specific call. In Python, successful response objects expose _request_id; for an exception, read the request-id header from its response.

Common exam distractor

Any answer that treats a 4xx as a model-quality problem, applies one blanket retry to every failure, or answers a 403 by rotating the API key is wrong. A 400 was rejected before the model ran; 400, 401, 403, 404 and 413 need the request or credentials changed; a rate-limit 429 and 5xx are retried with backoff, but a spend-cap 429 is not.

Escalation

Escalate when the failure reproduces on a clean configuration, or when several users see 5xx or 529 at once. Check the Claude status page first; it publishes incidents for the Claude API, Console and Claude Code among others. Then assemble a packet: Anthropic request IDs, timestamps with time zone, model and provider, Claude Code version, /status output, a minimal repro, what you ruled out, and the impact. Claude Code's /feedback command and the GitHub issue tracker cover product bugs; account and billing problems go to Anthropic support.

Mind hygiene: a /heapdump snapshot contains the full conversation and credentials, so attach only its -diagnostics.json file to a public issue. On your own platform, keep a runbook built on the triage table above and alert on 5xx and 529 rates separately from 4xx rates, because they imply different owners.

Exam traps

Practice question

In a monorepo, developers report that Claude Code ignores the rule 'run pnpm lint before finishing' stored in services/billing/CLAUDE.md. The rule is followed when Claude starts by reading or editing an existing file under services/billing/, but not when the first action is creating a new file there. /status shows no managed-settings problem. What is the most likely explanation and next step?

  • A The model has regressed on instruction following; open an Anthropic support ticket now, attaching the request IDs from the affected sessions and the date the problem started.

    The pattern (works after a read, fails after a create) is deterministic and points to how the file loads, so a model regression is an unsupported jump. Escalate only after ruling out your own layers.

  • B The user-level ~/.claude/CLAUDE.md is read last and therefore overrides the subdirectory rule whenever the two files disagree about how linting should run.

    User-level files are read first, not last, and reading order is not a precedence mechanism. Files are concatenated, and the symptom depends on the first action, which points at loading.

  • C Subdirectory CLAUDE.md files load only when Claude reads a file there, not when it creates one. Check /context, then move the rule to the root, a path-scoped rule or a hook. Correct

    The docs state that subdirectory files load when Claude reads a file there and not when it writes or creates files. /context shows whether the file is in the Memory files list. A root-level file or a path-scoped rule loads reliably, and a hook is the option for must-always-run behaviour.

  • D Add Bash(pnpm lint) to permissions.allow in the shared settings file so the command is permitted without a prompt for every developer on the team.

    An allow rule removes a permission prompt; it does not make Claude decide to run the command. The evidence suggests the instruction was never in context, not that the command was blocked.

Build exercise: Fault-injection lab and escalation runbook

Intermediate · 75 minutes

You'll practice:

  1. In a scratch repository, inject five faults: (a) a hook whose matcher is a JSON array, (b) a skill saved as .claude/skills/deploy.md instead of a folder with SKILL.md, (c) a .mcp.json placed inside .claude/, (d) a rule stored in services/api/CLAUDE.md that you test by asking Claude to create a new file there, (e) a deny rule Bash(rm *) tested against a scratch file removed with /bin/rm. For each, record the symptom, the first command you ran, the layer, and the fix.

    These are documented, common causes of 'my configuration is ignored'. Reproducing them trains you to reach for the loading diagnostics before rewriting prompts.

    You should see: Five rows of notes. For (a) claude doctor reports the settings problem and /hooks shows nothing from that file; (b) the skill is absent from /skills; (c) /mcp shows no server; (d) /context lacks the file until Claude reads a file in that directory; (e) the deny rule does not stop the alternate form.

    Hints
    1. For each fault, which single command would show whether the thing loaded at all?
    2. Use /hooks, /skills, /mcp, /context and /permissions for the first look, claude doctor for invalid settings, and compare against the debugging table in the docs for the fix.
    3. Fixes: matcher becomes a string such as "Edit|Write"; skill moves to .claude/skills/deploy/SKILL.md; .mcp.json moves to the repository root with servers under mcpServers; move the rule to root or a path-scoped rule (or a hook if it must always run); use a sandbox or PreToolUse hook when a Bash deny must hold.
  2. Run a bisect on a session that misbehaves: first claude --safe-mode, then a clean session using an empty CLAUDE_CONFIG_DIR from a directory with no project configuration. Reintroduce user config, project config and MCP servers one at a time. Write a bisect log naming the step that reintroduced the problem.

    A methodical bisect turns 'something is off' into a named cause and avoids changing several things at once.

    You should see: A log with one row per step (safe mode, clean dir, +user config, +project config, +MCP) and a clear PASS or FAIL per row, ending in a single culprit.

    Hints
    1. What does safe mode disable, and what does it deliberately leave on?
    2. If safe mode fixes it, the cause is CLAUDE.md, skills, plugins, hooks, MCP servers, commands or agents. If not, use a clean config directory and check /status for managed settings and environment variables.
    3. Commands: claude --safe-mode; then cd /tmp && CLAUDE_CONFIG_DIR=/tmp/claude-clean claude (expect first-run screens and a fresh login); finally copy files back one by one, or launch from the project directory, and re-test after each change.
  3. Write a small Python script that makes three failing calls (an invalid model name, an invalid request, and a client with an obviously fake key) and prints the status code, the request-id header, the elapsed time and the message for each, catching the most specific exception classes first. Confirm that none of the 4xx responses was retried.

    It shows typed exception handling, the request ID on the exception's response, and the deterministic-4xx rule in one place.

    You should see: Three lines of output with different status codes (record what you observe rather than assuming), a non-empty request-id for each response the API returned, and elapsed times near a single round trip rather than several seconds of backoff.

    Hints
    1. Which exception class is the parent of the status-specific errors, and which one covers network failures with no response?
    2. Catch anthropic.APIStatusError for anything that carried a status, anthropic.APIConnectionError separately, read e.status_code and e.response.headers.get('request-id'), and time each call.
    3. import time, anthropic
      MSG = [{'role': 'user', 'content': 'hi'}]
      def probe(label, client, **kw):
          t = time.time()
          try:
              client.messages.create(**kw)
          except anthropic.APIStatusError as e:
              print(label, e.status_code, e.response.headers.get('request-id'), round(time.time() - t, 2), str(e)[:100])
          except anthropic.APIConnectionError as e:
              print(label, 'no response', e.__cause__)
      ok = anthropic.Anthropic()
      probe('bad-model', ok, model='not-a-real-model', max_tokens=16, messages=MSG)
      probe('bad-request', ok, model='claude-sonnet-5', max_tokens=16, messages=[])
      probe('bad-key', anthropic.Anthropic(api_key='sk-ant-obviously-fake'), model='claude-sonnet-5', max_tokens=16, messages=MSG)
  4. Correlate a Claude Code failure across logs: start a session with --debug-file ./cc-debug.log and OpenTelemetry console exporters enabled, provoke a failing API call, then find the same failure in the debug log and in the claude_code.api_error event and record the request_id and session.id.

    Operations teams need one identifier that links a user's report, the client log and the server-side record.

    You should see: An api_error event with the error details and (when the server returned one) request_id, plus a client_request_id and matching lines in the debug file. Your notes state which identifier you would give support.

    Hints
    1. Which environment variables switch telemetry on, and which exporters print to the console?
    2. Set CLAUDE_CODE_ENABLE_TELEMETRY=1 with the logs exporter set to console, run claude with --debug-file, cause a failure such as an invalid model name, and search both outputs for the error.
    3. CLAUDE_CODE_ENABLE_TELEMETRY=1 OTEL_LOGS_EXPORTER=console claude --debug-file ./cc-debug.log, then grep -n 'error' ./cc-debug.log and look for a claude_code.api_error event in the console output. Prompt text is not logged unless you opt in.
  5. Write an escalation packet template and a one-page triage table for your platform. Fill the template with the failure from step 3 or 4, check the Claude status page, and state whether you would escalate and to whom.

    The packet is what turns an anecdote into an investigation, and the triage table tells on-call staff which layer to check first.

    You should see: A template with request IDs, timestamps and time zone, model and provider, Claude Code version, /status output, minimal repro, ruled-out causes and impact, and a triage table that separates 4xx (owner: application team) from 5xx and 529 (check the status page, then support).

    Hints
    1. What would an engineer who has never seen your system need to reproduce the problem?
    2. Include identifiers first, then environment, then the smallest repro, then what you tried. Add a rule about not attaching credentials, transcripts or heap snapshots.
    3. Template: 1) Impact and time window; 2) request-id values; 3) model, provider, SDK or Claude Code version; 4) /status output; 5) minimal repro; 6) ruled out (config bisect result, status page check); 7) attachments (diagnostics only, no keys).

Sources