It drives me nuts when a coding agent stops while there is still plenty of work left. You ask for a refactor, tests and a deployment; it completes the refactor, runs one reassuring command and signs off with “Looks good now.” The answer sounds so settled that it is easy to accept the mood before checking the work.
Codex’s /goal mode suggested the control loop I wanted. The objective had to remain visible while the transcript grew, survive compaction and retain its authority amid plausible partial results. Claude Code did not yet have a native equivalent, but its plugin hooks exposed enough machinery to build one.
claude-code-goal keeps an objective active until Claude explicitly promises that it is complete, I stop or pause it, or an iteration ceiling is reached. Geoffrey Huntley’s Ralph loop supplied the central technique: a Stop hook can prevent a turn from ending and feed a continuation prompt back into Claude Code.
The Stop hook
When Claude tries to finish, the plugin loads the active goal and examines the last assistant message for an exact tag:
<promise>GOAL_COMPLETE</promise>
Natural language is too accommodating for this job. “I think this is fixed,” “everything should work,” and “looks good now” might be responsible qualifications or premature exits; a hook cannot reliably tell which. The promise tag makes completion an explicit act.
Without it, the hook returns decision: block. Claude Code turns the reason into a synthetic user message and starts the next iteration:
$ echo '{"session_id":"demo","cwd":".","last_assistant_message":"Looks good now."}' \
| plugins/goal/bin/goalctl.py hook-stop | python3 -m json.tool
{
"decision": "block",
"reason": "Continue working toward the active Claude Code goal.\n[...]\nMost recent assistant summary, for continuity:\nLooks good now.\n",
"systemMessage": "Goal iteration 2 / 30"
}
The continuation prompt puts the original objective back in view and asks Claude to inspect files, command output, tests, repository state and every explicit requirement before considering the promise. Claude can still emit the tag while mistaken, but leaving the loop now requires a deliberate claim that can be inspected instead of a change in conversational tone.
In a live run, the agent completed part of the task and tried to stop. The hook returned the outstanding objective and continued the same session; work from the first pass remained in place, and the next one finished what had been left behind.
I considered using another model as the completion judge, but its decision would have been harder to inspect than an explicit marker in the project file. For this plugin I preferred a blunt handshake whose failure I could see.
The goal belongs to the project
The other decision I cared about was where the state should live. An opaque service would have made the plugin harder to understand at precisely the moment it misbehaved, so each goal is an ordinary markdown file:
.claude/goals/current.goal.md
.claude/goals/<session-id>.goal.md
Frontmatter records the status, session, iteration count, completion promise and timestamps. The body contains the objective, continuation notes, iteration history and every compact summary. If the session drifts, I can open the file and see the original request at the top. If a hook does something surprising, I can use a text editor and git diff. Writes go through mkstemp, write and replace so an interrupted update does not leave half a goal behind.
SessionStart and UserPromptSubmit remind resumed sessions that a goal is active, while PostCompact saves the compacted summary beside the objective. I also tend to have several terminals open in one project. Slash commands do not always receive Claude Code’s session ID, so /goal:goal creates current.goal.md when it cannot identify the session; the first hook with a real ID adopts it, and hooks from other sessions ignore it. The behaviour is pinned by test_hook_adopts_current_state_when_command_has_no_session_env.
Where the loop ends
Claude Code only invokes these hooks during its own lifecycle. Killing the process or interrupting the user bypasses Stop, and state stays on the local machine. The iteration ceiling limits continuations, not tokens or cost; --max-iterations 0 removes even that protection. Scheduling, retries after process failure and work across machines belong in an orchestrator.
The promise is deliberately escapable too. A typo in a custom tag could otherwise strand the session, so /goal:complete and /goal:stop provide manual exits. The plugin exists to help me finish work, not to argue with me about when I am allowed to leave.
Installation is two commands and a Claude Code restart:
claude plugin marketplace add chrischabot/claude-code-goal
claude plugin install goal@claude-code-goal
Claude Code added a native /goal in version 2.1.139 on 12 May 2026. It keeps the objective in the session and asks a fast model to judge completion after each turn, although that evaluator cannot run tools or inspect files independently. The file-backed plugin remains a different design: a deterministic stop hook and a project-owned ledger that both the agent and I can open, inspect and diff.