Debugging with Claude Code in the terminal
Debugging with Claude Code is a four-step terminal loop: pipe the error in with its context, investigate in plan mode so nothing is edited while the cause is unknown, prove the root cause with a test that fails, then fix until that test and the full suite pass. git bisect run and headless claude -p cover regressions and CI failures.
This page is for developers who debug in the terminal with Claude Code, and for tech leads who decide what a bug-fix pull request must prove. The situation: CI is red with Cannot read properties of undefined (reading 'map'), the stack trace crosses six files in two services, and the last merge touched 40 files. You paste the error and ask for a fix. Claude adds a null check, CI goes green, and the same crash returns next week from a different caller.
The agent did not fail at coding. It was asked to edit before anyone knew the cause. The workflow below keeps it searching until the cause is proven, and makes the proof a test you can check without reading every line of the fix.
What you’ll walk away with from terminal debugging
Section titled “What you’ll walk away with from terminal debugging”- A pipe-first way to hand Claude Code test, server, log and CI output without copy-paste
- A read-only investigation step in plan mode, and a rule for when to leave it
- Ten copy-paste prompts: the bug report, the constrained fix, production stack traces, type errors, race conditions, memory growth, CI-only failures, suspect commits without a known-good one, parallel investigation with subagents, and CI triage
- A
git bisect runrecipe that survives flaky tests - An evidence checklist a reviewer can approve without reading the whole diff
- Recovery steps for the six ways Claude Code debugging goes wrong
How does the Claude Code debugging loop work?
Section titled “How does the Claude Code debugging loop work?”Each phase limits what Claude may change. The limit, not the model, is what stops the fix-break-fix cycle.
| Phase | Claude Code mode | Claude may change | Leave the phase when |
|---|---|---|---|
| 1. Hand over the failure | claude -p with a pipe, then claude -c | nothing | the error, trigger and recent changes are in context |
| 2. Investigate | plan mode (claude --permission-mode plan, or /plan in a session) | nothing | Claude names one cause, with the file, line and condition |
| 3. Prove the cause | your normal mode, test files only | a new test | the test fails for the reason Claude named |
| 4. Fix and verify | your normal mode | the files the diagnosis named | the new test and the full suite pass |
-
Hand over the failure with its context
Pipe the output straight in. Claude Code reads standard input together with the prompt, so the error never passes through your clipboard:
Terminal window npm test -- --run tests/services/order.test.ts 2>&1 | \claude -p "This test fails. Read the test and the source it imports. \Name the most likely root cause and the file to open first. Do not edit anything."Then run
claude -cto continue the same conversation interactively, with the output still loaded. When you know more than the log shows, spend 30 seconds on a structured report: -
Investigate in plan mode
Start the session in plan mode, or press
Shift+Tabuntil the mode indicator shows plan mode. Claude can read and search files, but it cannot edit them:Terminal window claude --permission-mode planAsk for the trace, not the fix: “Trace the request from
src/routes/orders.tsline 47 through the service layer into the query. Where doesitemsbecomeundefined?” Challenge the answer before you accept it: “Prove it. Show the line where the value originates and explain why it only happens for users with expired sessions.” Two plausible causes means you are still in phase 2. -
Prove the cause with a failing test
A diagnosis is a claim. A test that fails for the stated reason is evidence. Ask Claude to write the reproduction first, run it, and show you the failure message. If the test fails for a different reason, the diagnosis is wrong: go back to step 2 rather than fixing forward.
-
Fix, then run everything
Leave plan mode and give Claude a fix it cannot widen:
Then ask one more question: “Search the codebase for other callers that make the same assumption, and list them with file:line. Do not change them.” That list becomes the next ticket, not part of this diff.
How do you pipe logs and CI output into Claude Code?
Section titled “How do you pipe logs and CI output into Claude Code?”The pipe works wherever the error already prints. Filter before you pipe: a hundred relevant lines let Claude group errors by cause, while a hundred thousand raw lines get paraphrased.
Production logs:
grep -E "ERROR|WARN" /var/log/app/production.log | tail -100 | \ claude -p "Group these errors by root cause. For each group give the count, \the source file and line, and whether it relates to the payment timeout we are \investigating. Order by count."A failed GitHub Actions run (the gh CLI prints only the failing steps’ logs with --log-failed):
gh run view RUN_ID --log-failed | \ claude -p "Identify the failing test, read its source and the code it tests, \and explain what broke. Check the commits in this run for the change that caused it." \ --allowedTools "Read,Grep,Glob,Bash(git show:*),Bash(git log:*)"Replace RUN_ID with the number from gh run list.
Production errors straight from Sentry. When the trace lives in an error tracker rather than a log file, connect the Sentry MCP server so Claude reads the issue, its breadcrumbs and the release it started in:
claude mcp add -s project --transport http sentry https://mcp.sentry.dev/mcpclaude mcp login sentrySetup for Grafana, Datadog and PostHog, and the read-only scopes to grant, are on the observability MCP page.
Which Claude Code prompts fix recurring bug types?
Section titled “Which Claude Code prompts fix recurring bug types?”Production stack traces
Section titled “Production stack traces”The generic prompt gets a generic answer. This one forbids the generic fix and ends on a passing test:
Type errors that point at the wrong file
Section titled “Type errors that point at the wrong file”Race conditions and intermittent failures
Section titled “Race conditions and intermittent failures”A vague prompt gets a vague theory. Name the failure precisely and give Claude the checklist of what actually causes it:
For races that resist the first pass, raise the reasoning depth before you change the model. From v2.1.280 (the latest channel), Claude Code’s default model is Claude Opus 5.5, and it runs at medium effort; /model shows what your install uses. Put ultrathink in one prompt to deepen a single turn, or run /effort high (or start with claude --effort xhigh) for the session. The trade-offs are on the deep reasoning page, and current defaults and prices on the models hub.
Memory growth
Section titled “Memory growth”Heap snapshots are often hundreds of megabytes of JSON, and reading one floods the context. Compare them yourself first: load both in Chrome DevTools, open the Memory panel’s Comparison view, and paste the top retainers into the prompt.
Passes locally, fails in CI
Section titled “Passes locally, fails in CI”How do you find the commit that broke it?
Section titled “How do you find the commit that broke it?”When you have a good commit and a test that reproduces the bug, let git do the search and Claude do the reading. Run this in your terminal:
git bisect start HEAD GOOD_SHAgit bisect run npx vitest run src/api/search/search.test.tsgit bisect log | claude -p "Read this bisect log. Name the first bad commit, \read its diff, and explain which change breaks /api/search." \ --allowedTools "Read,Grep,Glob,Bash(git show:*),Bash(git log:*)"git bisect resetReplace GOOD_SHA with the last commit you know worked. git bisect run treats exit code 0 as good and 1–127 (except 125) as bad, so any test command works.
Without a known-good commit, start from time instead:
How do subagents help with a bug that spans services?
Section titled “How do subagents help with a bug that spans services?”When a bug crosses several parts of the system, subagents investigate in parallel, each in its own context, and return a summary. Your main session keeps only the findings, which leaves room for the fix. Claude Code ships a built-in Explore subagent for this kind of search; you can also define your own in .claude/agents/ (see custom subagents).
To have this loop available without prompting for it each time, install a debugging skill. Claude loads it whenever a bug report matches the skill’s description:
npx skills add obra/superpowers --skill systematic-debugging# ornpx skills add mattpocock/skills --skill diagnosing-bugsThe portable npx skills add copy of Superpowers has no SessionStart bootstrap, so the full Superpowers method does not fire on its own. For that, install the plugin in a Claude Code session instead: /plugin install superpowers@claude-plugins-official.
How to check that a skill actually changed the agent’s behaviour is on testing and debugging skills.
How do you triage CI failures headlessly?
Section titled “How do you triage CI failures headlessly?”claude -p runs without a person at the keyboard, and --output-format json wraps the answer in JSON that a later step can post as a pull request comment. For triage, keep the job read-only: allow only the file-reading tools and deny every permission prompt instead of waiting for one.
--bare --setting-sources "" --strict-mcp-config keeps the branch’s own hooks, settings and .mcp.json from loading next to the key; --bare authenticates only with ANTHROPIC_API_KEY or an apiKeyHelper, not with a subscription login. In a workflow, run the tests and the claude -p call in separate jobs and pass test-output.txt as an artifact, as the test-triage workflow does: a test step can write to GITHUB_ENV or GITHUB_PATH and so reach every later step of its own job, including the one that holds the key.
Two rules for running this in a workflow. Run it only on your own branches (push, or pull_request from the same repository), never with secrets on a pull_request_target or workflow_run trigger that checks out contributor code. And let a person or a separate, reviewed job apply fixes: a triage job that can edit and push turns a flaky test into an unreviewed commit. The full workflow, permissions block included, is on the Claude Code CI/CD page.
How do you verify a Claude Code bug fix without reading every line?
Section titled “How do you verify a Claude Code bug fix without reading every line?”Review the evidence, not the diff. A bug-fix pull request is ready when it carries these five items, and the reviewer checks them in this order:
- Root-cause statement: one sentence naming file, line and triggering condition.
- Red before green: the new test’s failure output on the unfixed code, then its pass. Check it yourself: stash only the fix with
git stash push -- <the files the diagnosis named>, run the new test and see it fail, thengit stash pop. A plaingit stashalso stashes a test added to an existing test file, so the red run exercises the old tests and proves nothing. - Full suite: the summary line of the complete test run after the fix, not only the new test.
- Scope:
git diff --statlists only the files the diagnosis named, and no existing test changed. - Similar callers: the list of other places with the same assumption, filed as follow-up work.
The developer who ran the session signs off on items 1–3. The reviewer signs off on items 4 and 5, and reads the diff in full only when item 4 shows files outside the diagnosis. The same items fit the team-wide evidence bundle format.
What breaks when you debug with Claude Code?
Section titled “What breaks when you debug with Claude Code?”Claude fixes the symptom. It adds a null check and the crash moves elsewhere. Cause: it was asked to fix before the cause was known. Recovery: undo the edit with /rewind (or Esc twice), return to plan mode, and use the bug-report prompt. Say what you want fixed: “the value is null because the fetch races the render; fix the race, not the null check.”
The fix breaks something else. Only the new test was run. Recovery: make “run the full suite and paste the summary” part of every fix prompt, and have CI run the complete suite as a required check.
The new test passes before the fix. The test does not reproduce the bug, so it proves nothing. Recovery: run it on the unfixed code (git stash push -- <the fix files>, not a plain git stash, which can stash the test too), and if it passes, tell Claude “this test passes without the fix, so it does not reproduce the bug; make it fail for the reason you named.”
Claude edits the test to make it pass. A weakened assertion or a new .skip turns red into green without fixing anything. Recovery: check git diff --stat for test files, add the deny rule from the caution above, and re-run the original test from the commit where you wrote it.
Bisection blames the wrong commit. A flaky test marks commits good or bad at random. Recovery: git bisect reset, then make each step run the test several times so one failure marks the commit bad: git bisect run sh -c 'for i in 1 2 3 4 5; do npx vitest run test.ts || exit 1; done'.
The context fills with logs. Long sessions accumulate file reads and test output until early findings drop out. Recovery: run /compact keep the root-cause statement, the failing test and its output, or start a fresh session with only the diagnosis and the test name. Piping filtered output, not whole logs, prevents it.