# Test and debug your agent

When an agent does something wrong, you debug it the same way you debug any
program with one extra step: first you find out what it did. Read the
trace of its steps and tool calls, reproduce the bad run, then fix the single
weak step instead of rewriting the whole prompt.

## Why do agents need a different debugging loop?

A normal function is deterministic: same input, same output, so a stack trace tells the whole story. An agent
is not. The model decides what to do at each step, so the same task can take a
different path twice. That means "it broke once" is not a bug report yet. You
need the trace of what happened, and a way to make it happen again.

So the loop has four moves: observe what the agent did, reproduce it reliably,
isolate the one step that went wrong, then change that step and re-run to
confirm. Most of the work is the first two. Once you can see the run and repeat
it, the fix is usually small.

## Step 1: observe what the agent did

You cannot fix a step you cannot see. An agent run is a sequence of turns: the
model thinks, calls a tool, reads the result, and decides again. Your first job
is to read that sequence in order.

The Claude Code CLI [records every session](/guides/claude-code-session-state), and you can replay one. Resume the
most recent run in the current directory, or pick a past session from a list:

```bash
# Continue the most recent session here
claude --continue

# Pick from a list of past sessions, or resume one by id or name
claude --resume
claude --resume auth-refactor
```

To read a run as data instead of scrolling the terminal, ask for verbose,
turn-by-turn output. This shows each tool call and each tool result, which is
exactly the part you need:

```bash
claude --verbose
```

For a one-shot run you want to capture and inspect later, use print mode with a
structured output format. `stream-json` emits one JSON event per line as the
agent works, so you can save it and grep through it:

```bash
# Emit one JSON event per line; --verbose is required for stream-json
claude -p "Summarize the failing tests" \
  --output-format stream-json --verbose > run.jsonl
```

Each line in that file is one event: a user message, an assistant message, a
tool call, a tool result, or a system event. Reading them in order is the trace.These saved trace and debug files capture raw tool results, so they often hold API keys, tokens, or file contents the agent read. Keep them out of git and delete them when you are done, do not paste one into a shared issue.

### What should I look for in the trace?

In the trace, you are looking for the first step that goes wrong, not the final
error. Watch for these:

| Symptom in the trace | What it usually means |
| --- | --- |
| Tool called with bad arguments | The tool's description or schema is unclear |
| Agent never calls a tool it should | The tool is missing, misnamed, or not allowed |
| Tool returns an error, agent retries blindly | The result is not telling the model how to recover |
| Agent loops on the same action | No stopping condition, or it cannot tell it succeeded |
| Right tools, wrong conclusion | The task framing or context is the problem |

The last column is the real bug. The visible error is often three steps
downstream of it.

### Turn on debug logs for the plumbing

If the problem is lower down, in tool wiring or an MCP connection, raise the
log level. Debug mode prints internal events, and you can filter to the
categories you care about so the output stays readable:

```bash
# Show API and MCP internals only
claude --debug "api,mcp"

# Write debug output to a file for a long run
claude --debug-file /tmp/agent-debug.log
```

If the failure is an MCP server that will not connect at all, that is its own
fix path: see [fix an MCP server that won't connect](/guides/fix-an-mcp-server-that-wont-connect).

## Step 2: reproduce the bad run

A bug you cannot repeat is a bug you cannot fix, and with a non-deterministic
system that is the hard part. You reduce the randomness by pinning everything
around the model.

**Pin the inputs.** Save the exact prompt and the exact files or data the agent
saw. If the agent reads `src/auth/session.ts`, snapshot that file at the version
it failed on. A run that reads different inputs is a different run.

**Save the prompt as a fixture.** Keep the failing prompt verbatim so you can
fire it again unchanged. A reusable, copy-paste prompt looks like this:

```text
Reproduce the failing checkout flow.

Context:
- the order total is wrong when a coupon is applied
- the agent runs against src/checkout/total.ts
- the test that fails is tests/checkout.test.ts

Do this:
- read total.ts and the failing test
- run the test and show the actual vs expected total
- do not change any code yet, just report what you find
```

**Re-run in isolation.** Run the repro in a clean, separate checkout so a fix
experiment cannot corrupt your main working tree. A git worktree gives you a fresh
directory on its own branch from the same repo:

```bash
# Create an isolated checkout on a new branch
git worktree add ../agent-repro -b agent-repro

# See all worktrees you have open
git worktree list
```

Now you can run the agent against `../agent-repro` and throw it away afterward.
For more on this pattern, see [isolate parallel agents with git worktrees](/guides/isolate-parallel-agents-with-git-worktrees).

**Accept that one run is not proof.** Because the path varies, run the repro a
few times. If it fails three times out of five, you have a real reproduction. If
it fails once in twenty, you are chasing a rare path and should still pin more
inputs before you trust any fix.

## Step 3: isolate the weak step

Now narrow from "the run failed" to "this step failed". Walk the trace forward
and stop at the first step whose result is wrong. Everything after that is the
agent reacting to a bad result, so ignore it for now.

Once you find the step, sort the cause into one of three buckets. The bucket
tells you what to change.

### Is it the tool?

The model called the right tool but got a bad result, or could not call it at
all. Check the tool's definition. A tool the agent uses well has a clear name, a
one-line description of when to use it, and a typed schema for its arguments.
Here is a tool described as config, the way the agent sees it:

```json
{
  "name": "get_order_total",
  "description": "Return the final total for an order, including any coupon. Use this before quoting a price to the customer.",
  "input_schema": {
    "type": "object",
    "properties": {
      "orderId": { "type": "string", "description": "The order id, e.g. ord_123" },
      "couponCode": { "type": "string", "description": "Optional coupon code" }
    },
    "required": ["orderId"]
  }
}
```

If the agent passed a coupon in the wrong field, the schema or its descriptions
were not specific enough. Tighten the wording, mark fields required, and the
agent calls it correctly. For the underlying mechanic, see
[tool calling explained](/guides/tool-calling-explained).

### Is it the context?

The agent had the right tools but the wrong understanding. It was missing a
file, given stale data, or buried under so much input that it lost the point. The
fix is what goes into the run, not the code: feed it the exact file, drop the
noise, and state the goal plainly. This is its own skill, covered in
[feed Claude the right context](/guides/feed-claude-the-right-context) and
[context engineering for AI agents](/guides/context-engineering-for-ai-agents).

### Is it the instruction?

The tools and context were fine, but the task was vague, so the model guessed. A
prompt like "fix the checkout" leaves too much open. Rewrite it to name the
file, the expected behaviour, and the stopping condition. The repair pattern
lives in [fix a prompt that isn't working](/guides/fix-a-prompt-that-isnt-working).

## Step 4: fix one thing and re-run

Change exactly one thing, then run the pinned repro again. If you change the
tool schema and the prompt and the context at once, a passing run tells you
nothing about which change worked, and a still-failing run tells you even less.
One change per loop keeps the signal clean.

Keep the change tied to the bucket you found:

- **Tool bucket:** sharpen the description, make the schema stricter, or make
  the tool return a clearer message on failure so the agent can recover.
- **Context bucket:** add the missing file, remove the irrelevant input, or
  state the goal at the top of the prompt.
- **Instruction bucket:** name the target file, the success condition, and what
  not to touch.

Then run the saved repro again, several times, and compare against the bucket of
runs from step 2. You are not looking for one lucky pass. You want the failure
rate to drop close to zero across repeated runs.The trap is declaring victory on the first green run, because a non-deterministic agent can pass once for the same random reason it sometimes failed. One pass moves your evidence from "fails most runs" to "unknown", not to "fixed". Re-run the pinned repro the same number of times you ran it in step 2 and compare failure rates before you trust the change.

## Add guardrails so the fix sticks

A debugged agent regresses the moment the prompt drifts or a tool changes. Two
habits keep it fixed.

**Turn the repro into a check.** Keep the failing prompt and its pinned inputs
as a saved eval, and run it before you ship any change to the agent. It is a
[regression test for a non-deterministic system](/guides/evals-for-production-ai). The bug you fixed becomes the
test that proves it stays fixed.

**Gate risky actions with hooks.** A hook can block or confirm a dangerous tool
call before it happens, so a bad step is caught at runtime instead of in a
post-mortem. See [hooks and guardrails](/guides/hooks-and-guardrails).

## The whole loop in one pass

Put together, debugging an agent is short once you can see the run:

1. **Observe.** Replay the session with `--verbose`, or capture
   `--output-format stream-json` to a file, and read the trace in order.
2. **Reproduce.** Pin the prompt and inputs, run in an isolated git worktree,
   and confirm it fails on most runs, not just one.
3. **Isolate.** Find the first wrong step and sort it into tool, context, or
   instruction.
4. **Fix.** Change one thing, re-run the pinned repro, and watch the failure
   rate drop.
5. **Lock it in.** Save the repro as an eval and add a hook for anything risky.

The shift from normal debugging is small but it is the whole game: you are
debugging a process that does not repeat itself, so you spend your effort making
it repeat before you try to fix it. Once you can see the run and reproduce it,
the fix is almost always one clear, checkable change.
