cd /news/ai-agents/show-hn-makoto-the-claude-code-plugi… · home topics ai-agents article
[ARTICLE · art-53809] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Show HN: Makoto the Claude Code plugin that keeps the AI honest

Makoto is a plugin for Claude Code that intercepts the AI agent's tool calls and blocks those that fabricate evidence, weaken verifiers, or make unsubstantiated claims, enforcing honesty by checking the agent's actions against its own statements.

read14 min views1 publishedJul 10, 2026
Show HN: Makoto the Claude Code plugin that keeps the AI honest
Image: source

An integrity hook for Claude Code that watches the agent's own tool calls and blocks the ones that fake a check. When Claude says it did something (ran the tests, cited a paper, committed the fix, verified the certificate), makoto holds that word against its record. If the deed isn't there, or the verification was quietly disabled, makoto blocks the tool call (or the end-of-turn) and hands the agent a one-line correction to retry against.

It judges the agent against its own utterances and record, never the world's truth. It holds no facts ("France doesn't exist to it"); it only checks that a claimed word is kept, whole, and honored in deed. A word it lets through becomes spendable: trustworthy tender a reviewer or another agent can accept without re-deriving it.

makoto fires on mechanical hook events: every PreToolUse

, PostToolUse

, Stop

, and (advisory-only; see ConfigChange watch below) ConfigChange

. It blocks (exit 2, which Claude Code treats as block-and-retry) on 22 pre-checks across five families and 14 end-of-turn gates (plus gate.stale_establisher

and gate.undeclared_falsifiable

, two permanently-advisory catalog-completeness checks outside this 14-gate contract entirely: 16 live Stop-tier checks in total). Every pre-check and every one of the 14 end-of-turn gates but two blocks; there is no silent "warning" tier for those (see Fire level). The two documented exceptions are gate.self_wired

and gate.canon_fingerprints_advisory

(below), advisory-only checks that by design never block.

Verifier weakening: a check silently neutered

content.verifier_predicate_weakened

loose-comparator verifier (startswith

/endswith

/re.match

where==

is meant)content.verifier_exit_masking

exit-code masking (|| true

,; true

,set +e

on a test/build/lint)content.verifier_body_hollowed

hollowed verifier body (return True

/pass

in a constitution check)content.env_gated_audit

audit/verification code gated behind an env var ·content.integrity_suppression_flag

integrity-named suppression flag (*_skip = true

)

Fabricated evidence: a claim with no backing artifact

content.phantom_citation

phantom citation (Author-Year not inmakoto/docs/CITATIONS.md

)content.unsourced_webfetch

WebFetch of a URL never seen in any prior tool result this sessioncontent.fabricated_commit_sha

fabricated commit SHA/tag presented as proof of a commitcontent.deferred_checkbox_theater

DEFERRED

-style checkbox theater on an open to-do itemcontent.illusory_authorship_trailer

an illusory AI-authorship commit trailer the agent never actually wrote

Security-verifier disabling: each a named CWE

content.cert_verify_disabled

/content.cert_none_mode

/content.cert_reqs_none

TLS / peer-certificate verification disabled (verify=False

,CERT_NONE

)content.jwt_signature_disabled

/content.jwt_none_alg

JWT signature verification off /none

algorithm whitelistedcontent.paramiko_host_key_weakened

paramiko SSH host-key check weakened toAutoAddPolicy

·content.timing_unsafe_compare

timing-unsafe==

on a secret/HMAC/digest

Self-defense

content.self_mute_guard

makoto self-mute (disabling or un-wiring makoto viasettings.json

)

Scope & contract discipline: illusory progress and out-of-contract action (SPEC-5, ported by shape from Assay)

event.thrash_revert

a whole-file Write that reverts a file to an earlier byte-identical content after an intervening different Write (A→B→A, no net progress)event.forbidden_location

a Write/Edit whose target resolves outside the working directory or into a protected system/credential locationgate.contract_order

a result-producing call issued while a declared Plan's dependency for that step is still undischarged (its Stop-time sibling gate guards the remainder at turn end)

End-of-turn gates: fire on the agent's closing claims, checked against the recorded ledger:

gate.completion

: "done / createdX

" but the artifact isn't on diskgate.advance

: advancing a phase whose precondition isn't recorded as metgate.green_claim

: "suite green" against a recorded test failuregate.dropped

: a forward promise carrying identifying info ("I'll adddef X

toY

", "3 tests inZ

") left undischarged at turn-end, said-but-not-done, checked against the agent's own touched-keysgate.fabricated_action

: "I ranX

/ executedY

" in a turn with no tool call at all (presence-of-work, not command-text match)gate.named_test

: "test_foo

passes" against a recordedFAILED

of that exact named test (coreference-pinned; the green_claim delta)gate.stale_pass

: "all tests pass" against pytest's ownlastfailed

record still naming a live failing test (the runner's ledger, existence-filtered for staleness)gate.liveness

: fires on the code itself, not a claim: a statement with no live effect inside a closed function (dead/illusory work the present-closure model can prove inert). It walks the turn's touched.py

ASTs, so it yields a finding per illusory statement rather than one per turn.gate.self_wired

:advisory only, never blocks(seeFire level). Partial-strip detection of makoto's own.claude/settings.json

hook wiring: fires ifPreToolUse

/PostToolUse

/Stop

is missing a makoto-dispatching entry while at least one other still has one. It has a documented blind spot: a single edit that strips all three simultaneously disables this check in the same instant it would have fired (Claude Code reloads hook config live, not once at session start), so it provides zero coverage against that full-strip case. See theConfigChange watchsection below, which closes exactly this gap when wired.gate.hollow_test

: fires on the code itself, not a claim: a HOLLOWED test (SPIRIT.md §4), one that survives in name while its content is gutted, so it can never actually fail. Four sub-patterns: no assertion of any kind in the test body; an asserted tautology (assert True

, or comparing an expression to itself); a broad, no-optry

/except

that silently swallows the only call-under-test's failure; and a test-shaped function that can never fire independently, either nested inside another function (pytest's collector never discovers it) or gated behind askipif

/skipIf

condition that is provably always true.gate.canon

:blocking(level="error"

, unlike the advisory exceptions below). Two language-agnostic Stop primitives over the turn's own call stream, reading only closed protocol fields (tool_name/tool_input identity,tool_response.interrupted

/.error

), no test-runner regex, no language token.canon.timeout

fires when the turn's LAST tool call ended in a direct error state (interrupted, or a self-emitted error code) and closed without resurfacing or resolving it; an error resolved by a later successful call stays silent.canon.recur

fires when the SAME tool call (identical name + byte-identical input) was re-issued back-to-back and every call in that consecutive run ended in the same direct error state: a stuck retry loop with nothing changed between attempts; a later success in the same run silences it. Certified via held-out adversarial RED fixtures (planted cases that must fire) plus a near-vacuous corpus-FP check: the honest corpus almost never carries the triggering precondition (interrupted

/self_error_code

) at all, so a clean corpus replay alone is inconclusive, not a certification (seeANCESTRY.md

§2 Part B for the corrected framing).gate.canon_fingerprints

:blocking. 4 of 17 ported session-level "canon" fingerprints (SPEC-5 Task 9, fromREF-lever-graded-primitives/signalminer/grade_planted.py

's 27-fingerprintTHE_CANON

) that are robust-core, blocking-capable by construction: 0-FP on both the planted-clean and real-Claude-gold negative sets per the gold-oracle certification (nogreen_checkdisabled

,nosrc_destruct

,nosrc_green_timeout

,notestedit_destruct

).gate.canon_fingerprints_advisory

:advisory only, never blocks. The other 13 of the same 17 fingerprints, each either resting on a soft/claim atom (claimed_pass_no_run

,tool_timeout

,assertion_weakened

) the gold-oracle finding doesn't certify as robust, or one of that finding's explicitly-named worst-disqualified fingerprints. Recorded to the audit log per SPEC-5's total-retention rule, never emitted as a block decision.gate.contract_order

:blocking. The Stop-time remainder guard over a declared Plan (SPEC-5, ported by shape from Assay'sContractOrder

pattern); fires when the turn ends with the plan's dependency remainder still non-empty. Its PreToolUse sibling guard is a pre-check, not a Stop gate; seemakoto/checks/contractOrder.py

.gate.stale_establisher

:advisory only, never blocks, opt-in. A declared Plan node recorded DONE whose named artifact no longer exists on disk (the one filesystem read this check family makes, ported by shape from Assay'sstale_establisher

). A product decision to escalate this to blocking is left to the caller; it is not made here.gate.undeclared_falsifiable

:advisory only, never blocks. A completeness audit over thechecks/

catalog itself: an orphan module the can't discover, or a declared id with no corresponding live module. A maintenance signal about makoto's own catalog, never a live integrity finding about the agent's turn.

Inspect the live catalog with makoto pattern list

; see one pattern in full with makoto pattern show content.phantom_citation

.

gate.canon_fingerprints

(and canon.timeout

within gate.canon

) read the session's own recorded call stream. Once a fingerprint's atoms go true they stay true forever, so without a real discharge path a single sanctioned action (e.g. an owner-approved destructive command) would otherwise block every remaining Stop for the rest of the session. The only discharge is an operator-attributed release, re-derived from the host-written transcript at check time and never trusted from ledger content, so no tool call or file write can forge it. Say, as a real message in the conversation (never inside a tool call or file write):

makoto release.operator <fingerprint-id>: <your reason>

makoto verifies the turn is genuinely user-authored, non-synthetic, and timestamped after the finding first fired, then discharges that exact fingerprint for the rest of the session. The discharge is chain-appended (kind="release.operator"

) for the audit trail; the block decision itself is always re-derived from the transcript, never read back from that row.

Annotate the line with makoto-allow: <reason>

(any comment style, case-insensitive). makoto won't fire on it, and your rationale is on the record: an auditable note, not a silent bypass.

if os.environ.get("ENABLE_AUDIT_TRAIL"):  # makoto-allow: app feature, gates user-facing audit logging
    write_audit_trail()

**See **: 誠 (makoto), the constitution every pattern derives from: a word is real the way water is wet, a constitutive property, not an after-the-fact audit.

docs/SPIRIT.md

/plugin install https://github.com/Clear-Sights/Makoto

Claude Code reads hooks/hooks.json

and registers PreToolUse

, PostToolUse

, and Stop

hooks pointing at ${CLAUDE_PLUGIN_ROOT}/_dispatch_shim.sh

automatically (which exec

s python -m makoto._dispatch

). ~/.claude/settings.json

is NOT modified; the plugin system manages its own hook registry.

State dir + makoto.db

are created lazily on the first hook invocation.

An illusory AI-authorship commit trailer reaches a commit through two doors. Pre-Check content.illusory_authorship_trailer

blocks the agent-authored one: the trailer typed into a git commit

message or into file content, the surface no setting can reach. The other door is Claude Code's own automatic append, which a setting governs. To close it at the source, set in ~/.claude/settings.json

:

{ "includeCoAuthoredBy": false }

This is defense in depth, not a replacement: the setting closes the auto-append door, content.illusory_authorship_trailer

closes the agent-authored one. makoto's install does not write this for you; it leaves settings.json

untouched beyond hook wiring (above); set it yourself if you want the earlier layer.

If you previously ran the old python -m makoto install

(0.3.0 or earlier), your ~/.claude/settings.json

has makoto-managed hook entries. Running the plugin alongside would cause double-dispatch. Migrate cleanly:

python -m makoto uninstall                   # removes old settings.json entries
/plugin install https://github.com/Clear-Sights/Makoto  # installs the plugin
pip install -e /path/to/makoto

The state dir and makoto.db

are created lazily on the first hook invocation; there is no separate init step.

/plugin uninstall makoto

python -m makoto uninstall   # removes makoto-managed settings.json entries

The state dir (~/.claude/makoto_state/

) is preserved on uninstall: audit.jsonl

and makoto.db

remain for forensic value. To fully reset, rm -rf

the dir.

python -m makoto status            # patterns loaded, hooks wired, state dir, any patterns muted
python -m makoto pattern list      # the full live catalog as a table
python -m makoto pattern show content.phantom_citation  # one pattern in detail
python -m makoto show src/auth.py  # ledger state for a normalized location key
python -m makoto install           # legacy: wire settings.json (prefer the plugin)
python -m makoto uninstall         # remove makoto-managed settings.json entries

If you want to inspect or hand-wire what the plugin does, add to the hooks.PreToolUse

, hooks.PostToolUse

, and hooks.Stop

arrays of ~/.claude/settings.json

:

{
  "matcher": "*",
  "hooks": [{"type": "command", "command": "python -m makoto._dispatch"}]
}

Bracket the additions with # makoto-managed-begin

/ # makoto-managed-end

markers for idempotent removal.

Code Meaning
0 No error finding. The tool call proceeds normally.
2 At least one error-level finding. Claude Code interprets exit 2 as block-and-retry: the tool call (or the turn, for a Stop gate) is blocked, the stderr diagnostic is surfaced to the agent as a tool-error message, and the agent retries with that feedback in context.

Every live pattern blocks (fire_level = "error"

→ exit 2). makoto deliberately has no non-blocking tier: a warning

/disabled

resting state (witnessing a violation and letting the tool through) is itself an illusory word, the exact weakening shape makoto exists to catch. The earlier three-tier system was removed in the 2026-06-02 warning-tier-elimination (a pattern either blocks at proven zero corpus-FP, or it is cut). This still governs all 22 pre-checks (_ALLOWED_FIRE_LEVELS = {"error"}

, enforced at load) and 12 of the 14 end-of-turn gates.

Two narrow, explicitly-recorded exceptions: gate.self_wired

(2026-07-05) and gate.canon_fingerprints_advisory

(SPEC-5 Task 9, DESIGN DECISION 26) fire at level="advisory"

, not "error"

, so each is recorded to the audit log but never emitted as a block decision. Neither is a reintroduction of the cut warning

tier. gate.self_wired

is a single, named check whose entire subject is makoto's own hook wiring, shipped advisory-only by explicit DESIGN DECISION as partial-strip detection, not prevention (it cannot see, and does not claim to see, a simultaneous full strip of all three hook entries; see the ConfigChange watch section, which closes exactly this gap when wired). gate.canon_fingerprints_advisory

covers 13 ported canon fingerprints that rest on a soft/claim atom or are explicitly disqualified against real-Claude gold, kept in the catalog at non-blocking advisory per SPEC-5's total-retention rule rather than dropped. Every other check keeps the invariant above unconditionally.

Each pattern carries a one-line, imperative retry_hint

telling the agent what to do instead. When a finding fires, the hint is printed on a second stderr line after the diagnostic:

[makoto ERROR] row content.verifier_predicate_weakened (verifier predicate weakened — loose-comparator shape): matched 'startswith(' at line 231
               retry: Use '==' for status comparison, not '.startswith()' / '.endswith()' / 're.match'. ...

Every dispatch appends one structured JSON line to $MAKOTO_STATE_DIR/audit.jsonl

(default ~/.claude/makoto_state/audit.jsonl

). It captures enough to triage true-positive vs. false-positive without leaking whole-file contents. It's plain JSONL; query it with jq

or any tool.

Field Description
ts
ISO-8601 UTC timestamp, microsecond precision
event
live.pre_tool_use live.stop (the firing events; PostToolUse is consumed for history)
hook_kind
Raw hook name from the harness
tool_name
The tool the agent invoked (Write , Bash , …)
session_id
Opaque session token
project_root
Absolute project root at invocation time
pattern_fires
List of pattern IDs that fired; [] if clean
exit_code
0 (clean) 2 (finding emitted, block-and-retry)
retry_hint_emitted
Boolean, at least one fired pattern had a non-empty retry_hint
findings
Per-finding {pattern_id, level, file, line, snippet}

Audit writes are best-effort. If the append fails (disk full, permission denied), dispatch prints one stderr line and continues with its original exit code. The audit subsystem cannot cause makoto to mis-block or mis-allow a tool call: a fundamental separation-of-concerns invariant.

Separate from the 22 pre-checks and 16 end-of-turn checks above: an optional ConfigChange

hook entry (_dispatch_configchange.py

) watches .claude/settings.json

edits for makoto's own hooks being stripped. Two tiers, both fail-open on any unexpected fault:

Advisory(unconditional): a settings edit that looks stripped, but with no evidence this exact path was ever genuinely wired, logs a stderr line + an audit row (gate.configchange_advisory

) and never blocks. The ambiguous "never wired vs. just stripped" case the underlying verdict predicate cannot resolve on its own.Blocking(gate.configchange_transition

): fires ONLY on a genuinely evidenced transition, either this exact settings path is in makoto's own install manifest (configchange_manifest.json

, written bypython -m makoto install

), or a PRIOR evaluation of this same path observed makoto's hooks present (configchange_snapshots.json

). A path with neither piece of evidence never blocks, no matter how many times it evaluates as stripped: a project that never had makoto's hooks wired must never be blocked from editing its own settings.

Never fires for policy_settings

(organization-managed policy is out of scope). Not yet part of the plugin-install path; wire it manually, the same way as the manual wiring below, via a ConfigChange

hook entry pointing at _dispatch_configchange.py

.

Makoto blocks the illusory word, but until this session's work, it never issued tender for the KEPT one. Here is the whole chain for one small, real, synthetic session (docs/demo/

; regenerate instructions there):

WORD: the agent writessrc/auth.py

, then claims"test_login passes now."

at Stop.DEED: the write lands (kind="touched"

); a test run fails (kind="testrun"

,FAILED tests/test_auth.py::test_login

); a fix lands; a second test run passes (kind="testrun"

,PASSED tests/test_auth.py::test_login

): three tamper-evident, hash-chained rows, each linked to the one before it.RECORD: the test-delta redirect (Task 3) fires on the pass/fail flip and is ITSELF chain-appended (kind="audit"

); the redirect's own firing is part of the permanent record, not just a line on someone's terminal.RECEIPT:makoto receipt --session demo-session-001

reports 2 claims, both trace-bound to averify_chain

-checkable row, 0 exemptions:

{
  "session_id": "demo-session-001",
  "verified_through": null,
  "claim_count": 2,
  "trace_bound_count": 2,
  "exemption_count": 0
}

The claim "test_login passes now."

is never re-derived by a human or a reviewer; it cites two specific rows anyone can independently re-verify with verify_chain

. That is the whole pitch: chained, receipted claims, not a linter that yells and leaves no trace.

docs/demo/render_demo.py

drives 3 REAL scenarios through the actual dispatchers (not the frozen corpus above) against a fresh, throwaway state dir each, and captures the genuine stdout/stderr:

Each SVG is rendered directly from that scenario's real logged stdout/stderr, not hand-written.

Regenerate: python docs/demo/render_demo.py && python docs/demo/render_svg.py

(the latter needs humanize

, pip install humanize

, for demo-only friendlier byte counts; never a core-package dependency, see that script's own docstring).

── more in #ai-agents 4 stories · sorted by recency
── more on @makoto 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-makoto-the-c…] indexed:0 read:14min 2026-07-10 ·