Skip to content
Yasser Alattas | MLOps, Kubernetes & SRE Blog
Go back

Write a Contract, Not a Request: The Seven Parts of a /goal Prompt

An unsupervised agent does not stop when it is confused. It stops when it decides it is finished. Those are very different events, and the gap between them is where your afternoon goes.

/goal in Claude Code closes part of that gap: it sets a condition that gets checked before the agent is allowed to stop. If the condition isn’t met, the agent keeps working. That single change turns a prompt from a request into a contract — and contracts have terms. Every term you leave out, the agent fills in for you, confidently, in the direction of “done.”

This post is the checklist I use before starting any long agent run: seven fields, why each one exists, what goes wrong when it’s missing, and a paste-ready skeleton at the end. It’s meant to be a reference you come back to, not a single-sitting read.

The /goal operating card — seven elements of an agent contract: task, why, outcome, constraints, verification, stop condition, and checkpoints, plus a paste-ready skeleton

Table of contents

Open Table of contents

TL;DR

What /goal actually does

In Claude Code (verified against 2.1.206), /goal is a small command with a large consequence:

/goal <condition>   # set the condition Claude must satisfy before stopping
/goal active        # show the currently active goal
/goal clear         # drop it and stop early

The mechanics, in the order you’ll experience them:

  1. You set a condition. It’s a single short line — there’s a character cap, so treat it as a headline, not a spec.
  2. You give Claude the actual task in your prompt, as normal.
  3. When Claude would otherwise stop, a session Stop hook fires and a checker evaluates the condition against what actually happened.
  4. The checker returns one of three verdicts: met (goal clears, you see “Goal achieved”), not yet (the run continues, with a recorded reason for why it wasn’t met), or impossible (the run halts with “Goal could not be achieved”).
  5. The goal survives session resume — it’s restored from the transcript, not held in memory.

Two operational constraints worth knowing before you plan a long run:

/goal is not /loop

Easy to conflate, very different tools:

/loop/goal
Triggera timer or self-paced intervalthe agent trying to stop
Ends whenyou stop ita checker says the condition is met
Use forpolling, watching CI, recurring checksdriving one task to a defined end state

/loop 5m check the deploy re-asks a question forever. /goal all tests pass and no TODOs remain refuses to let one task end early.

One more setting that matters

Claude Code’s own guidance for long-horizon agentic work is blunt: put the full task spec in one well-specified first turn, and run at high or xhigh reasoning effort. A two-hour unsupervised run is exactly the case where thinking budget pays for itself. Set it before you start, not after the first wrong turn.


The seven fields

Everything below is a field in the contract. The pattern for each: what it is, what the agent invents when it’s missing, and a template line.

01. Task — one objective

Bigger than one prompt, smaller than a backlog. One mission, never a list of unrelated work.

The failure mode isn’t ambition, it’s plurality. Give an agent three objectives and it will interleave them, and when it hits a wall on one it will quietly declare partial victory on the other two. A single objective gives the checker something binary to evaluate.

Bring <area> from <current state> to <target state>.

Concretely:

Bring src/ from datetime.utcnow() to timezone-aware datetime.now(timezone.utc).

Not: “Clean up the datetime handling and also fix the flaky tests and update the docs.” That’s three goals wearing one coat. Run them as three goals.

02. Why — intent, plus what to read first

The reason behind the task, plus the files, docs, issue or logs it must read before acting.

This is the field people skip, and it’s the one that decides quality. Intent is what lets the agent judge the trade-offs you didn’t foresee. Twenty minutes into a migration it will hit a call site that doesn’t fit the pattern. With intent, it reasons about your actual purpose. Without it, it picks whatever keeps the diff moving.

Pointing at reading material does something else: it front-loads the agent’s context with your conventions instead of the internet’s average. If your repo has a CONTEXT.md, an ADR folder, or a module that already does the thing right — name it.

<the reason this matters now>. Read <doc> and <file> first.

Python 3.12 deprecates utcnow() and we’re upgrading next sprint; naive datetimes have already caused two off-by-hours bugs in billing. Read docs/adr/0007-time-handling.md and src/core/clock.py first.

03. Outcome — the observable end state

Describe the finished world, not the steps.

Steps age badly. The moment the agent discovers your codebase isn’t shaped the way you assumed, a step list becomes a set of instructions it can’t follow and must reinterpret. An end state stays valid regardless of route.

The test for a good outcome line: could a stranger open the repo and say yes or no? If answering requires knowing what you meant, rewrite it.

No trace of <old thing> remains; <check> passes on every case.

No call site in src/ constructs a naive datetime; every timestamp written to the DB is UTC-aware; the full test suite passes.

Note what this does for the checker. “Refactor the datetime handling” gives it nothing to evaluate. “No call site in src/ constructs a naive datetime” is a claim it can go and verify.

04. Constraints — and what not to touch

Off-limits paths, conventions to obey, no new dependencies, the branch to work on.

Silence here is permission. An agent with a mandate to reach an end state and no stated boundaries will regenerate lockfiles, reformat files it merely passed through, add a dependency to save itself twenty lines, or edit the very test that’s blocking it. None of that is misbehaviour — you didn’t say not to.

The four constraints worth stating almost every time:

Don't touch <paths>. No new deps. Work on branch <name>.

Don’t touch migrations/ or tests/fixtures/. No new dependencies. Follow the helper style in src/core/clock.py. Work on branch chore/tz-aware-datetimes.

There’s a second reason to name the branch: it’s your undo. Everything below assumes you can throw the branch away and lose nothing.

05. Verification — a command, not an adjective

The checker runs real commands and reads the output. Give it ones that already run today.

This is where most goal prompts quietly fail. “Done when the code is clean” is not verification, it’s a mood. “Done when make lint exits 0” is verification, because someone can run it and disagree with you.

Three rules I’ve learned the expensive way:

Give commands that work right now. A check command that’s broken before you start is not a check — it’s a second task the agent will invisibly adopt, mid-run, without telling you. If pytest currently errors on collection, fix that before you set a goal that depends on it.

Write down the baseline. “The suite passes” means nothing if 12 tests were already failing. Capture the number first:

pytest -q 2>&1 | tail -1        # 412 passed, 12 failed  ← baseline
rg -c "utcnow\(" src/ | wc -l   # 37 call sites          ← baseline

Now your condition can be exact: 412+37 passing, 12 failing, zero utcnow( matches. Without the baseline, “all tests pass” is either impossible or accidentally satisfied.

Prefer checks that can’t be satisfied vacuously. rg "utcnow(" src/ returning nothing is a pass — and also what you get if the path is wrong, the tool isn’t installed, or the files were deleted. Pair every negative check with a positive one:

rg -c "datetime.now\(timezone.utc\)" src/   # must be >= 37
rg "utcnow\(" src/                          # must be empty
pytest -q                                   # must match baseline

Two commands, opposite directions, and a suite. Now “zero matches” means the work happened, not that the repo evaporated.

Done when: <command A> and <command B> both pass clean.

06. Stop condition

When to halt and ask instead of improvising.

Field 05 tells the agent how to prove success. This one tells it what failure looks like — and that failing loudly is an acceptable outcome. Without it, a blocked agent doesn’t stop; it routes around. It skips the file, marks the test xfail, adds a # type: ignore, and reports success. Every one of those is a rational move for something optimising a completion condition.

Three shapes worth giving it:

That last one has saved me more than the other two combined. It converts “the agent quietly rewrote half the repo” into a message you read before it happens.

Stop and report if <check> still fails after 3 attempts.

Claude Code has its own version of this: the checker can return an impossible verdict and halt with “Goal could not be achieved.” Your explicit stop conditions are what make that verdict fire early and legibly instead of after forty minutes of creative workarounds.

07. Checkpoints and a progress log

Commit in slices; append one line per checkpoint.

A two-hour unsupervised run produces one of two artifacts: a reviewable history, or a single enormous diff you have to accept or reject as a unit. The difference is one sentence in the prompt.

Commit per <slice>; append status to <log file>.

Commit per package (src/billing, src/api, src/workers). After each, append one line to MIGRATION_LOG.md: package, files changed, tests passing, anything skipped and why.

Three things this buys you:

  1. Bisectable history. When something breaks, git bisect finds the slice instead of you reading 3,000 lines.
  2. A record of judgement calls. The “anything skipped and why” column is the highest-signal output of the entire run. It’s where the agent tells you which of your assumptions were wrong.
  3. A resume point. Runs get interrupted. Committed slices plus a log mean the next run starts from a known state — and since the goal is restored on resume, it picks up the same contract.

The paste-ready skeleton

/goal <one objective, one sentence>

Why:         <intent>. Read <files/docs> first.
Outcome:     <the observable end state>
Constraints: Don't change <paths>. Follow <conventions>.
             No new deps. Work on branch <name>.
Done when:   <exact commands that must pass>
Stop if:     <blocker> — report, don't guess.
Log:         Commit per checkpoint, note it in <file>.

In practice, split it across the two inputs Claude Code gives you: the condition is one line, the spec is the prompt.

/goal zero `utcnow(` in src/, pytest matches the 412/12 baseline

Then, as the prompt:

Bring src/ from naive datetimes to timezone-aware UTC.

Why:         Python 3.12 deprecates utcnow() and we upgrade next sprint;
             naive datetimes caused two off-by-hours billing bugs.
             Read docs/adr/0007-time-handling.md and src/core/clock.py first.
Outcome:     No call site in src/ constructs a naive datetime. Every
             timestamp written to the DB is UTC-aware. Suite matches baseline.
Constraints: Don't touch migrations/ or tests/fixtures/. No new dependencies.
             Follow the helper style in src/core/clock.py.
             Work on branch chore/tz-aware-datetimes.
Done when:   `rg "utcnow\(" src/` is empty,
             `rg -c "datetime.now\(timezone.utc\)" src/` is >= 37, and
             `pytest -q` reports 412 passed / 12 failed (the baseline).
Stop if:     a call site needs a schema change — list them, don't migrate.
             Stop if pytest still fails after 3 attempts.
Log:         Commit per package (src/billing, src/api, src/workers). Append
             one line per package to MIGRATION_LOG.md: files changed, tests
             passing, anything skipped and why.

That’s ninety seconds of typing to buy two hours of unsupervised work with an audit trail. The trade is not close.

When to reach for /goal — and when not to

Reach for it when the work is mechanical, multi-file, and clearly terminating:

Don’t reach for it when:

The failure modes to watch for

Patterns I’ve hit, in rough order of how much time each one cost:

The vacuous pass. The check was grep on a path that no longer existed. Zero matches, goal met, nothing done. Fix: pair every negative check with a positive one.

The moved goalpost. The agent couldn’t make a test pass, so it changed the test. Technically the suite is green. Fix: put the test files in the “don’t touch” list, or check the test file hasn’t changed.

The silent skip. Four files didn’t match the pattern, so they got skipped, and the summary said “migrated all files.” Fix: the “anything skipped and why” line in the log, plus a positive-count check.

The scope explosion. A rename touched a shared type, and the shared type touched everything. Fix: a scope tripwire in the stop condition.

The uncommitted two hours. One diff, 3,000 lines, no history. Fix: field 07, always.

Every one of these is a missing contract term. That’s the whole thesis: the agent isn’t unreliable, it’s unconstrained, and constraints are text you either wrote or didn’t.

FAQ

Is /goal the same as an autonomous agent mode? No. It’s a stop condition. Claude works the way it normally does; the difference is that at the moment it would end the turn, a checker evaluates your condition and can send it back to work.

What happens if the goal genuinely can’t be achieved? The checker can return an impossible verdict, and the run halts with “Goal could not be achieved.” Explicit stop conditions in your prompt make that happen early rather than after a long stretch of workarounds.

Does a goal survive /resume? Yes — it’s restored from the transcript when you resume the session.

Why isn’t /goal working for me? Two common causes: you’re in an untrusted workspace (restart and accept the trust dialog), or hooks are restricted via disableAllHooks / allowManagedHooksOnly in settings or policy. The feature is implemented as a Stop hook, so restricting hooks disables it.

How long should the condition be? Short. There’s a character cap, and a long condition is a worse check anyway — a checker evaluating one crisp claim is more reliable than one adjudicating a paragraph. Headline in the condition, detail in the prompt.

Should I write this by hand every time? No — and this is the part people resist. Hand-written goals are consistently too thin, because you’re writing from the inside of the problem where the constraints feel obvious. Ask an agent that already has your repo context to draft the contract, then edit it. It will name paths and conventions you’d have left implicit, which is exactly the material the checker needs.

The one-line version

Spend the extra sixty seconds on the prompt. Every field you leave blank is a decision you’ve delegated to something optimising for “done” — and it will make that decision, every time, without telling you.

Write the contract.


Share this post on:

Next Post
ETL Strategies and Where Kafka Fits