The self-correction loop

Shipmoor Team
June 27, 2026
6 min read

The point of the harness is one closed loop: the agent edits, the edit is scanned, a finding goes straight back to the agent, and the agent fixes its own mistake before you ever see it. This page walks that loop on Claude Code, the harness’s primary agent, primarily through Claude Code’s native lifecycle hooks. The harness ships inside the shipmoor binary (bundled since 0.5.0), so everything here runs through shipmoor harness subcommands on the 0.5.1 product.

The wiring

shipmoor harness install claude

This writes two hooks into the top-level hooks key of .claude/settings.json, appended beside any hooks you already have (yours are never touched):

  • PostToolUse with matcher Edit|MultiEdit|Write|NotebookEdit: scans after every edit that mutates the working tree.
  • Stop: a final gate when the agent goes to declare itself done.

Each is a command entry that calls the harness handler back through the bundled binary: shipmoor harness hook claude-code post-tool-use and … stop:

"hooks": {
  "PostToolUse": [
    {
      "matcher": "Edit|MultiEdit|Write|NotebookEdit",
      "hooks": [{ "type": "command", "command": "shipmoor harness hook claude-code post-tool-use" }]
    }
  ],
  "Stop": [
    { "hooks": [{ "type": "command", "command": "shipmoor harness hook claude-code stop" }] }
  ]
}

Claude Code invokes the handler with the lifecycle payload on stdin. The handler scans the working-tree change with the real CLI (scan --changed, the CLI’s authoritative changed-set, which already includes the just-edited file) and answers in the project’s configured mode.

How it answers depends on the mode and the result:

ResultBlock modeFeedback mode
Finding to routeWrites the finding to stderr as [shipmoor-harness] … and exits 2 (Claude surfaces stderr to the model)Returns hookSpecificOutput.additionalContext with the finding and exits 0
Clean / nothing to observeexit 0, no-opexit 0, no-op
Tooling failure (CLI missing, scan failed)exit 0, no-opexit 0, no-op

A tooling failure degrades to a silent no-op on purpose: the harness never blocks an agent on its own problems.

The two events do different jobs. PostToolUse corrects the agent edit-by-edit, the moment a defect lands. Stop is the backstop: it gates before the agent declares done, so a change that slipped through can’t be handed back to you uninspected.

A real block → fix → clear cycle

Here is one concrete cycle, blow by blow.

The agent edits pay.py and adds an import to a local module that does not resolve (import receipt_engine) where no such file exists under the project’s module paths.

The PostToolUse hook scans the edit and blocks. In block mode it writes the finding to stderr and exits 2. Claude Code surfaces that stderr back to the model verbatim:

[shipmoor-harness] Shipmoor found 1 issue (1 high).

[high] pay.py:6 python.phantom_import
  Local module 'receipt_engine' is referenced but no file matches under PYTHONPATH.
  → 'receipt_engine' looks local but does not resolve from project module paths. Add the file, fix PYTHONPATH, or remove the import.

This is the honest contract in action: Shipmoor catches a change that did not earn its claim, an import to a module that isn’t there, and names the exact line and rule (pay.py:6 python.phantom_import) so the fix is unambiguous.

The agent self-corrects. Reading the finding, it removes the unresolvable import (or restores the file it meant to reference). No human is in this turn.

The re-scan clears. The hook runs again on the corrected edit, finds nothing to route, and exits 0 with no output. A clean change is always silent; the harness adds no noise to a session that doesn’t need it.

The Stop hook gates clean. When the agent goes to finish, the Stop hook runs one last pass. Because the change is now clean, it exits 0 and the agent is allowed to declare itself done. The loop closed without you in it.

Feedback mode: advise, don’t stop

Under feedback mode the same defect never blocks. The handler exits 0 and returns the finding in Claude Code’s documented hook-response shape, where it lands in the model’s context as additional information rather than a hard stop:

{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": "Shipmoor found 1 issue (1 high).\n\n[high] pay.py:6 python.phantom_import\n  Local module 'receipt_engine' is referenced but no file matches under PYTHONPATH.\n  → 'receipt_engine' looks local but does not resolve from project module paths. Add the file, fix PYTHONPATH, or remove the import."}}

The feedback text is the same in both modes, severity-ordered, path:line, rule ID, recommendation, rendered deterministically from the parsed scan report, never scraped from human-formatted output. Block mode just delivers it through stderr and exit 2 instead of additionalContext.

The loop is bounded

harness.max_feedback_cycles caps how many times findings are routed back within one loop. When the cap is reached, whatever remains is surfaced once rather than re-prompted, so a finding the agent genuinely can’t fix can never trap the session in an endless ping-pong. The bound is the safety valve that makes “feed it back automatically” safe to leave on.

Where the structured record lives

The hooks above route feedback inline, into the model’s context or onto stderr, and do not write a report file by default. If you also want the structured record, run shipmoor harness scan or shipmoor harness watch beside the session; either writes .shipmoor/last-watch.json (schema shipmoor.harness.report.v1).

That report is an envelope around the scan findings:

FieldWhat it carries
schemaAlways shipmoor.harness.report.v1.
modeThe effective mode the scan ran under (observe, feedback, or block).
pathsThe project-relative files the scan covered.
counts_by_severitySeverity name → count, from the scan summary.
exit_codeThe harness wrapper’s own exit code (0 ok, 1 blocked, 2 harness/scan error).
feedbackThe deterministic, human-readable text rendered from the findings, the same string fed back inline.
findings[]shipmoor.scan.v1 finding objects, forwarded verbatim from shipmoor scan --json.

Because findings[] is the canonical scan finding forwarded field-for-field, the harness and the CLI can’t drift in what a finding looks like. See Contracts & schemas for the full shape.

The same loop on Cursor

Cursor reaches the identical block → fix → clear loop through its own hook surface: its stop hook returns a followup_message carrying the finding, which Cursor hands back to the agent the same way Claude’s additionalContext does. Same scan, same deterministic feedback, different transport. See With Cursor.

Legacy note: pre-bundle installs wired a standalone shipmoor-harness script. That name is still recognized as an alias, but the supported form is the shipmoor harness … subcommand.

Next

  • With Claude Code: full setup, the settings.json it writes, and reversibility.
  • With Cursor: the same loop through Cursor’s stop hook.
  • Watch mode: run a scan-on-save beside the session and write the report file.
Last updated on June 27, 2026

Was this article helpful?

Your response is saved on this device.