Throw Errors as Agent Trajectory Corrections

James Phoenix
James Phoenix

When AI agents drive development, they mutate state. They create files, run scripts, generate configs. Sometimes they skip a step or do something in the wrong order. Traditional error messages describe what went wrong for a human. But if the primary consumer of that error is an agent, the message should tell it how to recover.

The pattern: make throw new Error() messages into prompt instructions that redirect the agent’s trajectory.


The Problem: Agents Break Invariants Silently

Consider a monorepo where git worktrees must be created through a specific setup script. The script allocates deterministic ports, creates database schemas, and generates helper scripts. An agent that skips this script and creates a worktree manually will produce something that looks correct but violates critical invariants: no schema, no port allocation, no migration scripts.

The agent doesn’t know it went wrong. Tests pass locally because they run against the main database. The invariant violation surfaces later as port collisions, schema conflicts, or silent data corruption across parallel agents.

This is the core issue: agents don’t have institutional memory about how things should be done. They infer from context, and when context is incomplete, they take plausible shortcuts.


The Solution: Error Messages That Are Prompts

Instead of:

throw new Error('Worktree .env file is missing required keys')

Write:

throw new Error(
  [
    `Worktree .env is missing required keys: ${missingKeys.join(', ')}`,
    '',
    'This worktree was not initialized with the setup script, or its .env was',
    'manually edited and is now incomplete. The setup script generates all',
    'required values (deterministic ports, isolated DB schema, task queue).',
    '',
    'To fix, re-run the setup script:',
    `  bash scripts/worktree/setup.sh ${projectRoot}`,
    '',
    'Skill reference: .codex/skills/worktree/SKILL.md',
    'Do NOT manually populate these values. The setup script derives',
    'deterministic ports from the worktree name to prevent collisions',
    'across parallel agents.'
  ].join('\n')
)

The first message tells a human what broke. The second tells an agent exactly how to fix it, which script to run, and where to find the full workflow documentation. Critically, it also tells the agent what NOT to do: don’t manually populate the values. Without that negative instruction, an agent’s most likely recovery is to guess at the values and write them into .env itself, which is exactly the wrong move.


What I Built: Five Trajectory Corrections in Vitest Global Setup

I implemented this pattern in a production monorepo’s vitest-global-setup.ts. This file runs before every integration test suite, making it the earliest possible interception point. If an agent has set up a worktree incorrectly, it finds out before any test code executes, not after 30 minutes of cascading failures.

The worktree setup script (scripts/worktree/setup.sh) does five things: validates the worktree name, creates an isolated PostgreSQL schema, derives deterministic ports via hash-based allocation, generates a .env with all values, and creates helper scripts (run-migrations.sh, reset-worktree-schema.sh). Each of those can go wrong independently when an agent skips the script. So I added five corresponding validations, each with an agent-aware error message.

1. Missing .env file

The most basic check. If no .env exists, the agent never ran setup at all. The error distinguishes between worktree context and root repo context, giving different instructions for each.

2. Missing required env keys

The .env exists but is incomplete. This catches the case where an agent created the file by hand or copied .env.example and edited a few values. The validation checks seven keys that the setup script generates: DATABASE_SCHEMA, API_PORT, WEB_PORT, MOBILE_PORT, WORKER_INSPECT_PORT, TEMPORAL_TASK_QUEUE, and WORKTREE_PORT_OFFSET. The error names every missing key so the agent understands the scope of the problem.

3. Missing helper scripts

The setup script generates run-migrations.sh and reset-worktree-schema.sh with the worktree’s database URL and schema name baked in. If these don’t exist, migrations will target the wrong schema. The error tells the agent not to create these manually because they contain interpolated values that must come from the setup script.

4. Default port collision guard

This is the most subtle check. An agent might run setup.sh but also might copy .env.example directly, which contains API_PORT=8080 and WEB_PORT=3000. These are the main worktree’s default dev ports. Inside a worktree, these will collide with the main dev server, causing EADDRINUSE errors that are difficult to diagnose. The error explains the hash-based port allocation system so the agent understands why its ports need to be different.

5. Integration lock timeout

If another worktree holds the integration lock, the agent is stuck. The original error said Timed out waiting for lock. The new error explains the single-writer constraint, gives three options (wait, force-release for stale locks, or fall back to pnpm type-check:quiet), and references the skill. This matters because an agent’s default response to a timeout is to retry, which will also time out.

Bonus: Lingering process cleanup

When a previous API server process can’t be stopped, the error now gives the exact kill -9 command and the specific pid file to remove, instead of just reporting the failure. It also warns about the pnpm dev rule from the worktree skill, because running dev and integration tests concurrently is the most common cause.


Why Vitest Global Setup Is the Right Place

Every agent treats test failure as a signal to change course. The vitest globalSetup hook runs before any test file loads, which means:

  • Failures are immediate, not buried in test output
  • The agent hasn’t committed to a path yet, so recovery is cheap
  • It runs every time, unlike CLAUDE.md which the agent might not read
  • The error message lands directly in the agent’s context window

Compare this to documenting the same invariants in a skill file. The skill file is passive context. The agent has to read it, understand it, and follow it. The globalSetup error is active enforcement. The agent can’t proceed until the invariant is satisfied.


The Three-Part Pattern

Every trajectory correction follows the same structure:

1. State what’s wrong (the invariant violation)

Worktree .env is missing required keys: DATABASE_SCHEMA, API_PORT

2. Explain why (so the agent doesn’t try to patch around it)

Leanpub Book

Read The Meta-Engineer

A practical book on building autonomous AI systems with Claude Code, context engineering, verification loops, and production harnesses.

Continuously updated
Claude Code + agentic systems
View Book
This worktree was not initialized with the setup script.

3. Prescribe the fix (reference the script, skill, or command)

To fix, re-run the setup script:
  bash scripts/worktree/setup.sh <path>
Skill reference: .codex/skills/worktree/SKILL.md

The “explain why” step is load-bearing. Without it, the agent’s most natural response to “missing DATABASE_SCHEMA” is to add DATABASE_SCHEMA=something to the .env file. The explanation makes clear that the values are derived, not chosen, so manual population is wrong. The skill reference gives the agent a full document to load if it needs more context about the worktree lifecycle.


When Not to Do This

Not every error needs agent-specific instructions. Reserve this pattern for:

  • State mutations that have a “correct way”. Worktree creation, database migrations, environment setup.
  • Invariants that agents commonly violate. If you’ve seen an agent skip a step more than once, encode the correction.
  • Errors where the fix isn’t obvious from context. If the agent can figure it out from surrounding code, a normal error message is fine.

Don’t add prompt instructions to errors in hot paths or library code. This is for infrastructure setup, test harnesses, and initialization scripts where the agent is the primary operator.


Defense in Depth: Add a Claude Code Hook Upstream

throw new Error() is reactive. It catches the agent after the invariant is already broken. Even with clear instructions, the agent still burned a cycle doing the wrong thing. The next layer of defense is a Claude Code PreToolUse hook that blocks the wrong action before it runs.

In the same monorepo, the canonical failure mode is an agent running git worktree add directly instead of going through scripts/worktree/create.sh. The raw git command creates a worktree with no port offset, no seeded secrets, and no Postgres schema. Every downstream validation in vitest-global-setup.ts will fire, but the worktree is already in a broken state on disk.

The hook stops the agent at the tool-call boundary. When Claude Code tries to run a Bash command, the hook inspects it and denies anything matching git worktree add:

#!/usr/bin/env bash
# .claude/hooks/block-raw-git-worktree-add.sh
set -euo pipefail

payload="$(cat)"
tool_name="$(printf '%s' "$payload" | jq -r '.tool_name // empty')"
command_str="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty')"

# Escape hatch for recovery scenarios.
if <a href="/posts/allow-raw-git-worktree-add-1/">"${ALLOW_RAW_GIT_WORKTREE_ADD: }" == "1"</a>; then
  emit_allow; exit 0
fi

if printf '%s' "$command_str" | grep -qE '(^|[;&|[:space:]])git<a href="/posts/space/">:space:</a>+worktree<a href="/posts/space/">:space:</a>+add($|<a href="/posts/space/">:space:</a>)'; then
  reason=$'Raw `git worktree add` is blocked in this repo.\n\nUse the sanctioned entry point instead:\n\n    ./scripts/worktree/create.sh <branch-name> [base-branch]\n\nIt runs scripts/worktree/setup.sh so the new worktree has:\n  - a unique WORKTREE_PORT_OFFSET (parallel dev + integration tests),\n  - secrets seeded from the primary .env (R2, Sentry, Resend),\n  - a dedicated Postgres schema.\n\nIf you genuinely need the raw git command, set ALLOW_RAW_GIT_WORKTREE_ADD=1.'
  emit_deny "$reason"
  exit 0
fi

emit_allow

The hook is wired up in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-raw-git-worktree-add.sh"
          }
        ]
      }
    ]
  }
}

Three details that matter:

  • Precise regex anchoring. The pattern matches git worktree add at command boundaries (start, ;, &&, |, whitespace) but leaves the other subcommands (list, remove, prune, move, repair) untouched. Over-blocking is worse than under-blocking because it pushes the agent into weirder workarounds.
  • The denial reason is itself a prompt. Same three-part pattern as the thrown errors: state what’s wrong, explain why (port offsets, seeded secrets, schema), prescribe the fix (./scripts/worktree/create.sh). The agent reads the denial and immediately runs the correct command on the next turn.
  • An explicit escape hatch. ALLOW_RAW_GIT_WORKTREE_ADD=1 exists because there are genuine recovery scenarios (a broken worktree that needs surgical fixes). A hook with no escape hatch becomes the thing agents try to bypass with creative shell tricks. An explicit, named variable is cheaper to audit than a clever workaround.

Now the defense has two layers. The hook catches the common case at the tool call. If an agent somehow gets past it, runs setup incorrectly, or edits .env by hand, the vitest errors catch the resulting invariant violations before any test runs. Belt and braces. Each layer corrects a different class of mistake.


The Compound Effect

Every time you encode a trajectory correction into an error message, you’re building a safety net that gets stronger. The agent hits the error, follows the instruction, and succeeds. If the same agent (or a different one) makes the same mistake later, the correction is still there. Unlike CLAUDE.md instructions that an agent might not read, error messages are impossible to ignore because they halt execution.

This is the shift: stop thinking of throw new Error() as debugging output. Start thinking of it as the most reliable prompt injection point in your entire system.


Related

Topics
Agent Path CorrectionAi AgentsDevelopment PatternsError HandlingPrompt Engineering

Newsletter

Become a better AI engineer

Weekly deep dives on production AI systems, context engineering, and the patterns that compound. No fluff, no tutorials. Just what works.

Join 306K+ developers. No spam. Unsubscribe anytime.


More Insights

Cover Image for How to Easily Translate High Fidelity Prototypes into Functional Apps

How to Easily Translate High Fidelity Prototypes into Functional Apps

Vague specs do not converge. Scalar loss functions do. If you can hand the agent a number that says “you are 0.66 wrong,” it will close the gap on its own.

James Phoenix
James Phoenix
Cover Image for The Four-Layer Wall Around Your Library’s Public API

The Four-Layer Wall Around Your Library’s Public API

When an agent loop writes most of your library, the largest risk is not a bug in a feature. It is the loop helpfully exporting an internal helper, an experimental type, or a half-finished module. Once that ships in a minor release, you own it forever. Four package-level layers stop the loop from doing this without anyone having to remember.

James Phoenix
James Phoenix