← All skills

better-goal

v2.0.0MITlong runs

`/goal` looks like it keeps working until the job is done, and it does not: the condition is judged by a small model reading the transcript, so it grades what the run said rather than what is true, and Claude Code overrides the hook after eight consecutive blocks and reports that turn as completed. Nine turns of real work trips it, silently. This arms its own guard instead — a command Stop hook that runs the gates and decides by exit code — plus a watcher outside the turn loop, because a run that dies mid-turn never reaches a Stop hook at all. It also knows when to give up: a gate failing identically turn after turn disarms the run rather than re-sending the same failure at the price of the whole session prefix. Built from 114 real goal runs, where the most common follow-up was the word "resume", six times in a row.

Install
/plugin install better-goal@fledgeling-plugins

Needs the marketplace added first — how to do that.

Reach for it when

Hold a long autonomous run to a finish line using a Stop guard and a stall watcher this skill creates itself, with no dependence on the /goal command.

Not for

NOT for interval or polling work (use better-loop) or for a single task that finishes in one turn.

What ships with it

Say any of this

  • set a goal to ship the rest of the backlog
  • keep going until every item is complete
  • harden this goal
  • make a goal for this
  • has the goal been met?
  • why have you stopped?

Taken from the skill’s own trigger description — these are the phrases it listens for. You do not have to match them exactly.

What comes backIllustrative — written from the skill’s documentation, not captured from a run

set a goal to ship the rest of the backlog

arm.sh --dry-run   (writes nothing)

PREFLIGHT
  ✓ worklist grounded in the repo — 11 items with IDs
  ✗ 2 items have no gate command — added
  ✗ stop-hook block cap unset → 8 blocks, then the turn
    is reported as "completed". Raised.

DIFF
  hooks.Stop            +  guard.sh (command hook)
  BLOCK_CAP             -  unset    +  500

WRITES
  docs/goals/goal-backlog.md
  .claude/goals/backlog.json   gates decided by exit
                               code, not by transcript

ARMED
  guard     runs the gates at the end of every turn
  watcher   Monitor, for the turns that never end

Turn 4: tests failed identically for the third time.
Disarmed as stuck rather than re-sending it.

Easily confused with

Why it exists

A long run needs two things the conversation cannot give it: something that decides at the end of every turn whether the work is actually done, and something that notices when a turn stops ending at all. Claude Code ships /goal, which is neither.

Setting a goal registers one session-scoped prompt Stop hook whose entire text is your condition, capped at 4,000 characters. After every turn a small fast model reads the transcript and answers yes or no. That model runs no commands and opens no files. It judges what the run said, not what is true — and "all screens now match the mock" reads as a pass.

Then there's the part that ends the run without saying so. Claude Code overrides any Stop hook after 8 consecutive blocks, and reports that turn as completed. Nine turns of real work is enough. There's a second silent exit: the evaluator can return an impossible verdict, which clears the goal outright. And a goal changes no permissions, so in the default permission mode an unallowed tool call sits waiting — which from outside looks exactly like a model that gave up.

This skill was built from 114 real /goal invocations across 13 projects between June and August 2026. What people typed straight afterwards is the evidence:

resume (six times in a row) "Has goal been met? if not, why have you stopped" "are you still working?" "Re-enforce your need to complete the goal, don't stop until it's met"

And /goal /create-fleet-goal, typed 13 times, which sets the condition to the literal string /create-fleet-goal. The run starts correctly, because the main model reads it as a directive. The evaluator is then asked every turn whether "/create-fleet-goal" has been satisfied, which it never can be.

What it does

It arms its own mechanism rather than borrowing one. Two pieces, both created here:

A command Stop hookguard.sh — which runs your gate commands at the end of every turn and decides on exit codes. If they pass, the turn ends and the run is over. If they fail, it blocks with the failing gate, its output, the iteration count, the remaining budget and the path to the brief. That reason text becomes the run's next instruction, so it is written as one.

A stall watcherwatch.sh under Monitor — for the failure the guard structurally cannot see. A guard fires when a turn ends; a run wedged on a permission prompt at 3am never ends one, so no hook runs and nothing is reported. The watcher reads the ledger's timestamp from outside and emits a line when it goes stale, which wakes the session.

It stops at arming. It does not start the work; the armed run does that.

StepWhat happens
Grounds the goalReads ORCHESTRATOR.md, docs/features-to-triage/, specs, plans, the ledger and open worktrees, so "all remaining work" becomes a list with a count
Writes the gatesEvery claim in the goal gets a command that exits non-zero when it is false. A claim with no such command is named as unverifiable rather than quietly accepted
Writes the briefdocs/goals/goal-<slug>.md: the worklist with IDs, the gate commands, the blocked-item policy, the resource ledger, the concurrency cap, the stop conditions
PreflightsThe settings that end a run without an error, listed below
Shows the diffEvery settings change printed before/after. arm.sh --dry-run writes nothing
ArmsThe guard, then the watcher, then the ledger

The preflight

CheckWhy
Trust dialog, disableAllHooks, allowManagedHooksOnlyThe guard is a hook; any of these and it never runs
Permission modeA goal changes no permissions, so default mode stalls the run
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPDefault 8, then override. arm.sh raises it and records the prior value so disarm.sh can put it back
Every skill named in a gate reasonThe model cannot invoke a disable-model-invocation skill from a guard reason. /verify and /code-review are both in that set
Ports, processes, simulatorsTwo runs fighting over one simulator is a documented stall
Armed runs already in this repoTwo runs sharing one state file was a real collision; state is per-slug now, and the preflight shows what is live

Stop when the failure stops changing

The defect that costs the most is not a run that stops early. It is a run that does not stop at all, re-sending the same six failing tasks turn after turn and re-paying the whole session prefix each time — 91% of the input in the heaviest sessions measured.

So the guard fingerprints the failing set. The first failure blocks with the full output. An identical second failure blocks with the output withheld — it is already in the context, verbatim, from last turn — and asks for a different approach instead. An identical third disarms the run and says why:

better-goal: backlog: identical failure ×3 — disarmed as stuck

The threshold is stuck_after, default 3. A run that is making progress never reaches it, because the fingerprint moves.

Important The guard deliberately ignores stop_hook_active. Claude Code's own troubleshooting says a Stop hook should read that flag and exit early while it is true, and that advice is right for a hook needing one continuation; it is wrong for a goal, which needs many. Following it disarms the run on its second turn. The run is bounded by max_iterations, a deadline and the stuck-detector in the state file instead.

The state file records the session id, so the hook stays inert in every other session in the same project, and lives at .claude/goals/<slug>.json, so two runs in one repo do not collide.

Installing

/plugin marketplace add fledgeling-co/fledgeling-plugins
/plugin install better-goal@fledgeling-plugins

Using it

On its own, when you want a hardened run set up from scratch:

/better-goal divide the remaining work into md files in docs/features-to-triage
then use /ship-fleet:ship-fleet until all items are complete

After /goal has already misfired. "Has the goal been met?", "why have you stopped?", "you stopped despite the goal being set". It reads the ledger and the state file rather than guessing from a transcript compaction may have eaten.

Composed with the built-in, if you want the evaluator's judgment as well as the gates:

/goal /better-goal continue until every remaining item is shipped

It notices when the live condition is a slash-command name, says so in a line, and replaces it with one that can be settled. The guard is the mechanism either way; /goal is additive.

Once armed, scripts/status.sh answers "is it still going" from the ledger, and scripts/disarm.sh stops it.

The rules that keep it honest

A gate that cannot fail is not a gate. If no command could ever demonstrate the claim, it is named as unverifiable up front rather than dressed as verified.

Never arm past a failed preflight. A run that stalls on a permission prompt at 3am looks exactly like a run that finished.

Every run is bounded — iterations, a deadline, and a repeat count, all in the state file, so it ends on your terms rather than on the block cap's.

A wake carries new information or it does not happen. Re-sending output already in the context is the expensive failure, not a safe default.

The ledger is the answer. Progress is read from a file, never inferred from a transcript.

What's in the box

skills/better-goal/
  SKILL.md
  references/
    mechanics.md      how the guard, the block cap and /goal actually work,
                      with the binary and doc citations
    failure-modes.md  fifteen observed failures, each mapped to its fix
    gate-craft.md     turning a claim into a command that exits non-zero
    presets.md        the two recipes people type most often
    templates.md      the brief, the state file, the ledger, the settings block
  scripts/
    preflight.sh      the checks; read-only, exits 1 if anything blocks
    arm.sh            writes per-slug state, registers the guard, raises the
                      block cap, prints the Monitor call; --dry-run shows the diff
    guard.sh          the Stop hook: runs the gates, writes the ledger,
                      blocks, escalates on a repeat, disarms when stuck
    watch.sh          the stall watcher: STALL / RESUMED / DONE / ENDED / GONE
    status.sh         reads every armed run's state and ledger
    disarm.sh         <slug> | --all; restores the prior block cap
evals/evals.json      six cases plus the process evals
EVALS.md              the measured result against the no-skill baseline

Does it earn its place

Measured, not asserted: each case run twice from an identical fixture, once with the skill and once with nothing loaded, graded by an independent agent that saw each response alone with no arm label.

Across both harness skills, 32 of 33 structural assertions against the baseline's 12, and the grader preferred the skill arm in 8 cases out of 8.

Two results worth reading before you trust that number. On the /code-review case the baseline scored the same 3/3: a capable model with the docs finds that trap on its own, and the skill's headline claim is not what separates them there. And the arming case is confounded, because the fixture is deliberately thin and both arms spend real effort discovering it.

The run also found three defects in these skills, including a deadline parsed as local time that disarmed a goal ten hours early on AEST. All three are fixed; the scores were produced before the fixes, and before the rebuild around self-armed mechanisms. EVALS.md carries the per-case table, the ties, and what was not measured.

What it doesn't do

It can't verify a judgment with a command. "The portal feels like an extension of the brand" has no exit code. The skill says so plainly rather than pretending, derives a measurable proxy where one exists, and offers an experimental agent-type gate (which gets tools and can open the artifact) as a supplement. It is never a replacement for someone looking.

It doesn't survive /clear. A new conversation drops the session. The state file and ledger persist on disk, so a resumed session can re-arm against the same brief, but the turn count and timer reset.

It doesn't make the work correct. It makes stopping honest. A run with weak gates will pass weak gates.