# Keep your AI agent on track

An agent wanders when nothing tells it when to stop. To keep it on track, give it one clear goal, the smallest set of tools it needs, a hard limit on steps, a check that proves the work is done, and an explicit stop condition. Bound those five things and the agent finishes the task instead of looping on it.

## Why do agents go off track?

An agent is a loop. The model reads the situation, picks a tool, runs it, reads the result, and decides again. That loop is powerful, but on its own it has no idea when "done" is. If the goal is fuzzy, the agent keeps exploring. If it has every tool, it tries the wrong ones. If nothing checks the result, it declares victory on broken work, or worse, keeps "improving" forever.

Every fix below removes one reason to keep going. Think of it as drawing a box around the agent: clear walls in, a clear exit out.

## How do I give an agent a clear goal?

The single biggest lever is the goal itself. A goal the agent can verify against is a goal it can finish. A goal it has to interpret is one it will drift on.

A weak goal sounds like a wish. A strong goal names the file, the change, and the proof:

```text
Fix the failing test in src/auth/session.test.ts.

The session token should expire after 30 minutes, not 30 seconds.

Done means: that test file passes and no other test breaks.
Do not change the public API of src/auth/session.ts.
```

Notice what that prompt does. It points at one file. It states the intended behaviour. It defines "done" as a checkable fact (a test passes). And it draws a wall the agent must not cross (the public API stays stable). The agent now has a target and a fence.

Keep the scope to one outcome. "Fix this test" finishes. "Clean up the auth module" never does, because there is always one more thing to clean. If you have a big job, break it into outcomes and run them one at a time.

## How do I limit an agent's tools?

A tool the agent does not have is a path it cannot wander down. Give an agent file reads and a shell and it might start refactoring half the repo. Give a research agent the ability to write files and it might "helpfully" edit your code mid-investigation. For how to expose abilities deliberately in the first place, see [give your agent tools it can call](/guides/give-your-agent-tools).

In the Claude Code CLI you allow and deny tools in `settings.json` under `permissions`. The `allow` list runs without a prompt, `deny` blocks outright, and `ask` pauses for your approval:

```json
{
  "permissions": {
    "allow": [
      "Read(./src/**)",
      "Bash(npm test *)"
    ],
    "deny": [
      "Bash(rm *)",
      "Read(./.env)",
      "Read(./secrets/**)"
    ],
    "ask": [
      "Write(./src/**)"
    ]
  }
}
```

Read that as a box. The agent may read source and run tests on its own. It must never delete files or read secrets. It may write to source, but only after you say yes. The agent cannot wander outside those walls because the tools to do so are not on the table.

A good default is the smallest allow-list that lets the task succeed. Start narrow and widen only when the agent needs more. The same idea drives the [MCP fundamentals](/guides/mcp-fundamentals) for connecting outside tools: expose the few an agent needs, not everything you have.

### Match the tools to the job

Different jobs need different boxes. A useful pattern is one agent per shape of work, each with its own tool set.

| Agent | Goal | Tools it gets | Tools it is denied |
| --- | --- | --- | --- |
| Researcher | Read code, report findings | Read, Grep, Glob | Write, Edit, network |
| Fixer | Make one focused change | Read, Edit, run tests | delete, deploy |
| Reviewer | Comment on a diff | Read, Grep | Write, Edit, Bash |

A read-only researcher physically cannot leave a mess. A fixer that cannot deploy cannot ship a half-done change. The denial is the safety rail.

## How do I stop an agent from looping forever?

Even with a clear goal and tight tools, a loop can spin. The simplest backstop is a hard ceiling on how many turns the agent takes before it stops on its own.

When you define a reusable agent in the Claude Code CLI, you put it in a Markdown file under `.claude/agents/` with YAML frontmatter. The `maxTurns` field sets that ceiling, `tools` is the allow-list, and the body is the agent's standing instructions:

```markdown
---
name: test-fixer
description: Fixes one failing test without touching the public API. Use when a single test is red.
tools: Read, Edit, Bash
maxTurns: 12
permissionMode: default
---

You fix exactly one failing test.

Rules:
- Read the test and the file under test first.
- Make the smallest change that turns the test green.
- Run the test suite to confirm. Stop as soon as it passes.
- Never change exported function signatures.
- If you cannot fix it in a few steps, stop and explain what is blocking you.
```

`maxTurns` is the wall the agent hits if every other stop fails. Set it generously enough for a real task but low enough that a stuck agent gives up in seconds, not minutes. Twelve turns for a focused fix is plenty; a loop that needs fifty is usually lost.

`permissionMode` decides how cautious the agent is. The CLI supports modes including `default` (ask before risky actions), `acceptEdits` (auto-approve edits, gate the rest), `plan` (explore and propose, change nothing), and `bypassPermissions` (skip prompts, only for trusted sandboxed runs). For an agent you are still tuning, `default` or `plan` keeps a human in the loop.

## How do I make an agent verify its work is done?

A step limit stops a runaway agent, but it does not tell you the work is correct. For that you need a check: something objective the agent must satisfy before it can claim success.

The best checks are the ones you already run by hand:

- A test suite passes (`npm test` exits zero).
- A type check or linter reports no errors.
- A specific assertion holds, for example a known input now returns the right output.

State the check in the goal so the agent aims at it. Then enforce it with a hook. A hook is a command the CLI runs at a fixed point in the loop, and its exit code can block the agent from finishing. The full set of events and patterns lives in [hooks and guardrails](/guides/hooks-and-guardrails).

A `Stop` hook runs when the agent thinks it is done. If your check fails, the hook returns non-zero and the agent is told to keep working instead of stopping on broken output:

```json
{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "npm test --silent" }
        ]
      }
    ]
  }
}
```

Now "done" is not the agent's opinion. It is a passing test suite. The loop cannot exit until the check is green, which turns a fuzzy goal into a hard gate.Pointing a Stop hook at your whole suite (`npm test`) backfires when an unrelated test was already red: the hook blocks on any failure, so the agent keeps "fixing" code it was never asked to touch and burns its whole turn budget. Scope the check to the file the goal names (`npm test -- session.test.ts`) so the gate measures the one outcome you asked for.

You can also check work mid-flight. A `PreToolUse` hook runs before a tool fires and can refuse a dangerous call (for example, a write outside the allowed folder), while `PostToolUse` runs after and can flag a bad result early. The same gate, applied at different points in the loop.

## Define explicit stop conditions

A check proves success. Stop conditions cover the rest: the cases where the agent should give up rather than grind. An agent without an exit for failure will retry the same broken approach until it hits the step limit, wasting time and tokens.

Spell out, in the agent's instructions, exactly when to stop and report instead of pushing on:

```text
Stop and report back instead of continuing if any of these happen:
- The same test fails twice after your changes.
- The fix would require changing a public function signature.
- You need a tool or file you do not have access to.
- You are unsure the change is correct.

When you stop, say what you tried and what is blocking you.
```

This is the exit door in the box. Three kinds of stop condition cover almost everything:

- **Success.** The check passes. Stop and report the result.
- **Failure.** A clear sign the approach is wrong (repeated failures, a blocked path). Stop and report the blocker.
- **Budget.** Steps, time, or cost run out. Stop wherever you are and report progress.

An agent that knows all three never spins forever. It either finishes, gives up cleanly, or runs out of budget, and in every case it tells you why.A Stop hook and a failure stop condition can fight: a non-zero hook forces "keep working", so it overrides the agent's plan to give up after two tries and it grinds to maxTurns anyway. Let the hook exit zero once the agent reports a real blocker, or move that give-up check into the hook itself.

## Put the box together

Each control removes one reason to keep looping. Together they form a complete box: a clear way in, tight walls, and a clear way out.

| Control | What it stops | Where you set it |
| --- | --- | --- |
| Clear goal | Drift from vague scope | The prompt or agent body |
| Tool allow-list | Wrong or risky actions | `permissions` in `settings.json`, `tools` in the agent file |
| Step cap | Endless looping | `maxTurns` in the agent file |
| Check | Declaring fake success | A `Stop` hook running your tests |
| Stop conditions | Grinding on failure | The agent's instructions |

## A worked example

Say you are fixing a bug in an online-store app: the cart total ignores a discount code. Here is the bounded version.

The goal names the outcome and the proof:

```text
Fix the cart total in src/cart/total.ts so a valid discount code is applied.

Done means: src/cart/total.test.ts passes, including the discount cases.
Do not change the function names exported from total.ts.
If the test still fails after two attempts, stop and explain why.
```

The agent that runs it is read, edit, and test only, capped at a dozen turns, and a `Stop` hook runs the cart tests before it can finish. The agent reads the failing test, makes the smallest change, runs the suite, and exits the moment it is green. If it gets stuck, it hits a stop condition and reports back rather than rewriting the whole module.

That is the whole discipline. You are not making the agent smarter, you are making the finish line visible and the walls real. A bounded agent is a useful agent.

## Where to go next

Boundaries are step one. To run several bounded agents at once or hand work off cleanly, see the [Claude Agent SDK](/guides/claude-agent-sdk), which packages this loop, its permissions, and its tools as something you can build on. To connect an agent to outside tools without handing it the keys to everything, start with [MCP fundamentals](/guides/mcp-fundamentals).
