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.

Table of contents
Open Table of contents
TL;DR
/goal <condition>sets a stop condition. Claude checks it before ending the turn and keeps iterating until a checker agrees it’s satisfied, or declares it impossible.- The condition is the check, not the task. Your task spec still lives in the prompt. Write both.
- Seven fields make a complete contract: task, why, outcome, constraints, verification, stop condition, checkpoints.
- Verification must be a command, not an adjective. “Done when
pytest -qandrg -c 'utcnow\(' src/both return the expected result” — not “done when it’s clean.” - Capture the baseline first. A check that passes today proves nothing tomorrow.
- Reach for
/goalon mechanical, multi-file, clearly-terminating work. Don’t reach for it on taste calls or exploration.
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:
- You set a condition. It’s a single short line — there’s a character cap, so treat it as a headline, not a spec.
- You give Claude the actual task in your prompt, as normal.
- When Claude would otherwise stop, a session Stop hook fires and a checker evaluates the condition against what actually happened.
- 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”).
- 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:
/goalonly works in trusted workspaces. If you dismissed the trust dialog, restart and accept it./goalwon’t run if hooks are restricted —disableAllHooksorallowManagedHooksOnlyin settings or enterprise policy disables it, because the whole feature is built on the Stop hook.
/goal is not /loop
Easy to conflate, very different tools:
/loop | /goal | |
|---|---|---|
| Trigger | a timer or self-paced interval | the agent trying to stop |
| Ends when | you stop it | a checker says the condition is met |
| Use for | polling, watching CI, recurring checks | driving 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/fromdatetime.utcnow()to timezone-awaredatetime.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. Readdocs/adr/0007-time-handling.mdandsrc/core/clock.pyfirst.
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:
- Paths.
Don't touch infra/,don't modify anything under tests/fixtures/. - Dependencies.
No new dependenciesis one of the highest-value five words you can write. - Conventions. Point at them rather than describing them:
follow the patterns in src/core/clock.py. - Branch. Always. An unsupervised run on
mainis a bad afternoon waiting to happen.
Don't touch <paths>. No new deps. Work on branch <name>.
Don’t touch
migrations/ortests/fixtures/. No new dependencies. Follow the helper style insrc/core/clock.py. Work on branchchore/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:
- A budget of attempts.
Stop and report if pytest still fails after 3 attempts. - A named blocker.
If a call site needs a schema change, stop and list them — don't migrate the schema. - A scope tripwire.
If more than 60 files need edits, stop and report the list first.
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 toMIGRATION_LOG.md: package, files changed, tests passing, anything skipped and why.
Three things this buys you:
- Bisectable history. When something breaks,
git bisectfinds the slice instead of you reading 3,000 lines. - 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.
- 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:
- Framework, library, or provider version migrations
- End-to-end refactors — renaming a concept across a codebase
- Clearing a backlog of failing or skipped tests
- Mechanical sweeps: deprecations, lint rules, type annotations, log-format changes
- Anything where you can already write the command that proves it’s done
Don’t reach for it when:
- The work is open-ended exploration. “Figure out why latency is high” has no end state. Investigate first, then set a goal.
- The work is a taste call. API shape, naming, architecture. The checker can’t evaluate taste, so it will approve the first thing that compiles.
- The prompt contains unrelated errands. Split it.
- Your check commands are broken before you start. This is the disqualifier. If
make testfails for unrelated reasons today, you haven’t got a goal — you’ve got two.
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.