{"slug": "show-hn-makoto-the-claude-code-plugin-that-keeps-the-ai-honest", "title": "Show HN: Makoto the Claude Code plugin that keeps the AI honest", "summary": "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.", "body_md": "**An integrity hook for Claude Code that watches the agent's own tool calls and blocks the ones\nthat fake a check.** When Claude says it did something (ran the tests, cited a paper, committed the\nfix, verified the certificate), makoto holds that word against its record. If the deed isn't there,\nor the verification was quietly disabled, makoto blocks the tool call (or the end-of-turn) and hands\nthe agent a one-line correction to retry against.\n\nIt judges the agent against its *own* utterances and record, never the world's truth. It holds no\nfacts (\"France doesn't exist to it\"); it only checks that a claimed word is kept, whole, and honored\nin deed. A word it lets through becomes spendable: trustworthy tender a reviewer or another agent can\naccept without re-deriving it.\n\nmakoto fires on mechanical hook events: every `PreToolUse`\n\n, `PostToolUse`\n\n, `Stop`\n\n, and (advisory-only;\nsee [ConfigChange watch](#configchange-watch-advisory--evidence-gated-blocking) below) `ConfigChange`\n\n.\nIt **blocks** (exit 2, which Claude Code treats as block-and-retry) on **22 pre-checks** across five\nfamilies and **14 end-of-turn gates** (plus `gate.stale_establisher`\n\nand `gate.undeclared_falsifiable`\n\n,\ntwo permanently-advisory catalog-completeness checks outside this 14-gate contract entirely: 16 live\nStop-tier checks in total). Every pre-check and every one of the 14 end-of-turn gates but two blocks;\nthere is no silent \"warning\" tier for those (see [Fire level](#fire-level)). The two documented\nexceptions are `gate.self_wired`\n\nand `gate.canon_fingerprints_advisory`\n\n(below), advisory-only checks\nthat by design never block.\n\n**Verifier weakening**: a check silently neutered\n\n`content.verifier_predicate_weakened`\n\nloose-comparator verifier (`startswith`\n\n/`endswith`\n\n/`re.match`\n\nwhere`==`\n\nis meant)`content.verifier_exit_masking`\n\nexit-code masking (`|| true`\n\n,`; true`\n\n,`set +e`\n\non a test/build/lint)`content.verifier_body_hollowed`\n\nhollowed verifier body (`return True`\n\n/`pass`\n\nin a constitution check)`content.env_gated_audit`\n\naudit/verification code gated behind an env var ·`content.integrity_suppression_flag`\n\nintegrity-named suppression flag (`*_skip = true`\n\n)\n\n**Fabricated evidence**: a claim with no backing artifact\n\n`content.phantom_citation`\n\nphantom citation (Author-Year not in`makoto/docs/CITATIONS.md`\n\n)`content.unsourced_webfetch`\n\nWebFetch of a URL never seen in any prior tool result this session`content.fabricated_commit_sha`\n\nfabricated commit SHA/tag presented as proof of a commit`content.deferred_checkbox_theater`\n\n`DEFERRED`\n\n-style checkbox theater on an open to-do item`content.illusory_authorship_trailer`\n\nan illusory AI-authorship commit trailer the agent never actually wrote\n\n**Security-verifier disabling**: each a named CWE\n\n`content.cert_verify_disabled`\n\n/`content.cert_none_mode`\n\n/`content.cert_reqs_none`\n\nTLS / peer-certificate verification disabled (`verify=False`\n\n,`CERT_NONE`\n\n)`content.jwt_signature_disabled`\n\n/`content.jwt_none_alg`\n\nJWT signature verification off /`none`\n\nalgorithm whitelisted`content.paramiko_host_key_weakened`\n\nparamiko SSH host-key check weakened to`AutoAddPolicy`\n\n·`content.timing_unsafe_compare`\n\ntiming-unsafe`==`\n\non a secret/HMAC/digest\n\n**Self-defense**\n\n`content.self_mute_guard`\n\nmakoto self-mute (disabling or un-wiring makoto via`settings.json`\n\n)\n\n**Scope & contract discipline**: illusory progress and out-of-contract action (SPEC-5, ported by shape from Assay)\n\n`event.thrash_revert`\n\na 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`\n\na Write/Edit whose target resolves outside the working directory or into a protected system/credential location`gate.contract_order`\n\na 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)\n\n**End-of-turn gates**: fire on the agent's closing claims, checked against the recorded ledger:\n\n`gate.completion`\n\n: \"done / created`X`\n\n\" but the artifact isn't on disk`gate.advance`\n\n: advancing a phase whose precondition isn't recorded as met`gate.green_claim`\n\n: \"suite green\" against a recorded test failure`gate.dropped`\n\n: a forward promise carrying identifying info (\"I'll add`def X`\n\nto`Y`\n\n\", \"3 tests in`Z`\n\n\") left undischarged at turn-end, said-but-not-done, checked against the agent's own touched-keys`gate.fabricated_action`\n\n: \"I ran`X`\n\n/ executed`Y`\n\n\" in a turn with no tool call at all (presence-of-work, not command-text match)`gate.named_test`\n\n: \"`test_foo`\n\npasses\" against a recorded`FAILED`\n\nof that exact named test (coreference-pinned; the green_claim delta)`gate.stale_pass`\n\n: \"all tests pass\" against pytest's own`lastfailed`\n\nrecord still naming a live failing test (the runner's ledger, existence-filtered for staleness)`gate.liveness`\n\n: 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`\n\nASTs, so it yields a finding per illusory statement rather than one per turn.`gate.self_wired`\n\n:**advisory only, never blocks**(see[Fire level](#fire-level)). Partial-strip detection of makoto's own`.claude/settings.json`\n\nhook wiring: fires if`PreToolUse`\n\n/`PostToolUse`\n\n/`Stop`\n\nis 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 the[ConfigChange watch](#configchange-watch-advisory--evidence-gated-blocking)section below, which closes exactly this gap when wired.`gate.hollow_test`\n\n: 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`\n\n, or comparing an expression to itself); a broad, no-op`try`\n\n/`except`\n\nthat 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 a`skipif`\n\n/`skipIf`\n\ncondition that is provably always true.`gate.canon`\n\n:**blocking**(`level=\"error\"`\n\n, 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`\n\n/`.error`\n\n), no test-runner regex, no language token.`canon.timeout`\n\nfires 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`\n\nfires 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`\n\n/`self_error_code`\n\n) at all, so a clean corpus replay alone is inconclusive, not a certification (see`ANCESTRY.md`\n\n§2 Part B for the corrected framing).`gate.canon_fingerprints`\n\n:**blocking**. 4 of 17 ported session-level \"canon\" fingerprints (SPEC-5 Task 9, from`REF-lever-graded-primitives/signalminer/grade_planted.py`\n\n's 27-fingerprint`THE_CANON`\n\n) 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`\n\n,`nosrc_destruct`\n\n,`nosrc_green_timeout`\n\n,`notestedit_destruct`\n\n).`gate.canon_fingerprints_advisory`\n\n:**advisory only, never blocks**. The other 13 of the same 17 fingerprints, each either resting on a soft/claim atom (`claimed_pass_no_run`\n\n,`tool_timeout`\n\n,`assertion_weakened`\n\n) 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`\n\n:**blocking**. The Stop-time remainder guard over a declared Plan (SPEC-5, ported by shape from Assay's`ContractOrder`\n\npattern); 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; see`makoto/checks/contractOrder.py`\n\n.`gate.stale_establisher`\n\n:**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's`stale_establisher`\n\n). A product decision to escalate this to blocking is left to the caller; it is not made here.`gate.undeclared_falsifiable`\n\n:**advisory only, never blocks**. A completeness audit over the`checks/`\n\ncatalog itself: an orphan module the loader 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.\n\nInspect the live catalog with `makoto pattern list`\n\n; see one pattern in full with `makoto pattern show content.phantom_citation`\n\n.\n\n`gate.canon_fingerprints`\n\n(and `canon.timeout`\n\nwithin `gate.canon`\n\n) read the session's own recorded\ncall stream. Once a fingerprint's atoms go true they stay true forever, so without a real discharge\npath a single sanctioned action (e.g. an owner-approved destructive command) would otherwise block\nevery remaining Stop for the rest of the session. The only discharge is an **operator-attributed\nrelease**, re-derived from the host-written transcript at check time and never trusted from ledger\ncontent, so no tool call or file write can forge it. Say, as a real message in the conversation\n(never inside a tool call or file write):\n\n```\nmakoto release.operator <fingerprint-id>: <your reason>\n```\n\nmakoto verifies the turn is genuinely user-authored, non-synthetic, and\ntimestamped after the finding first fired, then discharges that exact fingerprint for the rest of the\nsession. The discharge is chain-appended (`kind=\"release.operator\"`\n\n) for the audit trail; the block\ndecision itself is always re-derived from the transcript, never read back from that row.\n\nAnnotate the line with `makoto-allow: <reason>`\n\n(any comment style, case-insensitive). makoto won't\nfire on it, and your rationale is on the record: an auditable note, not a silent bypass.\n\n```\nif os.environ.get(\"ENABLE_AUDIT_TRAIL\"):  # makoto-allow: app feature, gates user-facing audit logging\n    write_audit_trail()\n```\n\n**See **: 誠 (makoto), the constitution every pattern derives from:\na word is real the way water is wet, a constitutive property, not an after-the-fact audit.\n\n`docs/SPIRIT.md`\n\n```\n# In Claude Code:\n/plugin install https://github.com/Clear-Sights/Makoto\n# (or a local clone path)\n```\n\nClaude Code reads `hooks/hooks.json`\n\nand registers `PreToolUse`\n\n, `PostToolUse`\n\n, and `Stop`\n\nhooks\npointing at `${CLAUDE_PLUGIN_ROOT}/_dispatch_shim.sh`\n\nautomatically (which `exec`\n\ns\n`python -m makoto._dispatch`\n\n). `~/.claude/settings.json`\n\nis NOT modified; the plugin system manages\nits own hook registry.\n\nState dir + `makoto.db`\n\nare created lazily on the first hook invocation.\n\nAn illusory AI-authorship commit trailer reaches a commit through two doors. Pre-Check `content.illusory_authorship_trailer`\n\nblocks\nthe **agent-authored** one: the trailer typed into a `git commit`\n\nmessage or into file content, the\nsurface no setting can reach. The other door is Claude Code's own **automatic** append, which a\nsetting governs. To close it at the source, set in `~/.claude/settings.json`\n\n:\n\n```\n{ \"includeCoAuthoredBy\": false }\n```\n\nThis is defense in depth, not a replacement: the setting closes the auto-append door, `content.illusory_authorship_trailer`\n\ncloses\nthe agent-authored one. makoto's install does **not** write this for you; it leaves `settings.json`\n\nuntouched beyond hook wiring (above); set it yourself if you want the earlier layer.\n\nIf you previously ran the old `python -m makoto install`\n\n(0.3.0 or earlier), your\n`~/.claude/settings.json`\n\nhas makoto-managed hook entries. Running the plugin alongside would cause\ndouble-dispatch. Migrate cleanly:\n\n```\npython -m makoto uninstall                   # removes old settings.json entries\n/plugin install https://github.com/Clear-Sights/Makoto  # installs the plugin\npip install -e /path/to/makoto\n# Then add makoto hook entries to ~/.claude/settings.json manually; see \"Manual wiring\" below.\n```\n\nThe state dir and `makoto.db`\n\nare created lazily on the first hook invocation; there is no separate\ninit step.\n\n```\n# Plugin install path:\n/plugin uninstall makoto\n\n# Non-plugin / legacy settings.json path:\npython -m makoto uninstall   # removes makoto-managed settings.json entries\n```\n\nThe state dir (`~/.claude/makoto_state/`\n\n) is preserved on uninstall: `audit.jsonl`\n\nand `makoto.db`\n\nremain for forensic value. To fully reset, `rm -rf`\n\nthe dir.\n\n```\npython -m makoto status            # patterns loaded, hooks wired, state dir, any patterns muted\npython -m makoto pattern list      # the full live catalog as a table\npython -m makoto pattern show content.phantom_citation  # one pattern in detail\npython -m makoto show src/auth.py  # ledger state for a normalized location key\npython -m makoto install           # legacy: wire settings.json (prefer the plugin)\npython -m makoto uninstall         # remove makoto-managed settings.json entries\n```\n\nIf you want to inspect or hand-wire what the plugin does, add to the `hooks.PreToolUse`\n\n,\n`hooks.PostToolUse`\n\n, and `hooks.Stop`\n\narrays of `~/.claude/settings.json`\n\n:\n\n```\n{\n  \"matcher\": \"*\",\n  \"hooks\": [{\"type\": \"command\", \"command\": \"python -m makoto._dispatch\"}]\n}\n```\n\nBracket the additions with `# makoto-managed-begin`\n\n/ `# makoto-managed-end`\n\nmarkers for idempotent\nremoval.\n\n| Code | Meaning |\n|---|---|\n| 0 | No error finding. The tool call proceeds normally. |\n| 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. |\n\nEvery live pattern blocks (`fire_level = \"error\"`\n\n→ exit 2). makoto deliberately has **no\nnon-blocking tier**: a `warning`\n\n/`disabled`\n\nresting state (witnessing a violation and letting the\ntool through) is itself an illusory word, the exact weakening shape makoto exists to catch. The\nearlier three-tier system was removed in the 2026-06-02 *warning-tier-elimination* (a pattern either\nblocks at proven zero corpus-FP, or it is cut). This still governs all 22 pre-checks (`_ALLOWED_FIRE_LEVELS = {\"error\"}`\n\n, enforced at load) and 12 of the 14 end-of-turn gates.\n\n**Two narrow, explicitly-recorded exceptions:** `gate.self_wired`\n\n(2026-07-05) and\n`gate.canon_fingerprints_advisory`\n\n(SPEC-5 Task 9, DESIGN DECISION 26) fire at `level=\"advisory\"`\n\n,\nnot `\"error\"`\n\n, so each is recorded to the audit log but never emitted as a block decision. Neither\nis a reintroduction of the cut `warning`\n\ntier. `gate.self_wired`\n\nis a single, named check whose\nentire subject is makoto's own hook wiring, shipped advisory-only by explicit DESIGN DECISION as\npartial-strip *detection*, not prevention (it cannot see, and does not claim to see, a simultaneous\nfull strip of all three hook entries; see the ConfigChange watch section, which closes exactly this gap when wired). `gate.canon_fingerprints_advisory`\n\ncovers 13 ported canon fingerprints that rest on a\nsoft/claim atom or are explicitly disqualified against real-Claude gold, kept in the catalog at\nnon-blocking advisory per SPEC-5's total-retention rule rather than dropped. Every other check keeps\nthe invariant above unconditionally.\n\nEach pattern carries a one-line, imperative `retry_hint`\n\ntelling the agent what to do instead. When a\nfinding fires, the hint is printed on a second stderr line after the diagnostic:\n\n```\n[makoto ERROR] row content.verifier_predicate_weakened (verifier predicate weakened — loose-comparator shape): matched 'startswith(' at line 231\n               retry: Use '==' for status comparison, not '.startswith()' / '.endswith()' / 're.match'. ...\n```\n\nEvery dispatch appends one structured JSON line to `$MAKOTO_STATE_DIR/audit.jsonl`\n\n(default\n`~/.claude/makoto_state/audit.jsonl`\n\n). It captures enough to triage true-positive vs. false-positive\nwithout leaking whole-file contents. It's plain JSONL; query it with `jq`\n\nor any tool.\n\n| Field | Description |\n|---|---|\n`ts` |\nISO-8601 UTC timestamp, microsecond precision |\n`event` |\n`live.pre_tool_use` | `live.stop` (the firing events; `PostToolUse` is consumed for history) |\n`hook_kind` |\nRaw hook name from the harness |\n`tool_name` |\nThe tool the agent invoked (`Write` , `Bash` , …) |\n`session_id` |\nOpaque session token |\n`project_root` |\nAbsolute project root at invocation time |\n`pattern_fires` |\nList of pattern IDs that fired; `[]` if clean |\n`exit_code` |\n`0` (clean) | `2` (finding emitted, block-and-retry) |\n`retry_hint_emitted` |\nBoolean, at least one fired pattern had a non-empty `retry_hint` |\n`findings` |\nPer-finding `{pattern_id, level, file, line, snippet}` |\n\nAudit 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.\n\nSeparate from the 22 pre-checks and 16 end-of-turn checks above: an optional `ConfigChange`\n\nhook\nentry (`_dispatch_configchange.py`\n\n) watches `.claude/settings.json`\n\nedits for makoto's own hooks\nbeing stripped. Two tiers, both fail-open on any unexpected fault:\n\n**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`\n\n) and never blocks. The ambiguous \"never wired vs. just stripped\" case the underlying verdict predicate cannot resolve on its own.**Blocking**(`gate.configchange_transition`\n\n): fires ONLY on a genuinely evidenced transition, either this exact settings path is in makoto's own install manifest (`configchange_manifest.json`\n\n, written by`python -m makoto install`\n\n), or a PRIOR evaluation of this same path observed makoto's hooks present (`configchange_snapshots.json`\n\n). 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.\n\nNever fires for `policy_settings`\n\n(organization-managed policy is out of scope). Not yet part of the\nplugin-install path; wire it manually, the same way as the [manual wiring](#manual-wiring-fallback)\nbelow, via a `ConfigChange`\n\nhook entry pointing at `_dispatch_configchange.py`\n\n.\n\nMakoto blocks the illusory word, but until this session's work, it never issued tender for the\nKEPT one. Here is the whole chain for one small, real, synthetic session\n(`docs/demo/`\n\n; regenerate instructions there):\n\n**WORD**: the agent writes`src/auth.py`\n\n, then claims`\"test_login passes now.\"`\n\nat Stop.**DEED**: the write lands (`kind=\"touched\"`\n\n); a test run fails (`kind=\"testrun\"`\n\n,`FAILED tests/test_auth.py::test_login`\n\n); a fix lands; a second test run passes (`kind=\"testrun\"`\n\n,`PASSED tests/test_auth.py::test_login`\n\n): 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\"`\n\n); 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`\n\nreports 2 claims, both trace-bound to a`verify_chain`\n\n-checkable row, 0 exemptions:\n\n```\n{\n  \"session_id\": \"demo-session-001\",\n  \"verified_through\": null,\n  \"claim_count\": 2,\n  \"trace_bound_count\": 2,\n  \"exemption_count\": 0\n}\n```\n\nThe claim `\"test_login passes now.\"`\n\nis never re-derived by a human or a reviewer; it cites two\nspecific rows anyone can independently re-verify with `verify_chain`\n\n. That is the whole pitch:\nchained, receipted claims, not a linter that yells and leaves no trace.\n\n`docs/demo/render_demo.py`\n\ndrives 3 REAL scenarios through the actual dispatchers (not the frozen\ncorpus above) against a fresh, throwaway state dir each, and captures the genuine stdout/stderr:\n\nEach SVG is rendered directly from that scenario's real logged stdout/stderr, not hand-written.\n\nRegenerate: `python docs/demo/render_demo.py && python docs/demo/render_svg.py`\n\n(the latter needs\n`humanize`\n\n, `pip install humanize`\n\n, for demo-only friendlier byte counts; never a core-package\ndependency, see that script's own docstring).", "url": "https://wpnews.pro/news/show-hn-makoto-the-claude-code-plugin-that-keeps-the-ai-honest", "canonical_source": "https://github.com/Clear-Sights/Makoto", "published_at": "2026-07-10 06:31:37+00:00", "updated_at": "2026-07-10 07:06:17.526776+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-ethics", "developer-tools"], "entities": ["Makoto", "Claude Code"], "alternates": {"html": "https://wpnews.pro/news/show-hn-makoto-the-claude-code-plugin-that-keeps-the-ai-honest", "markdown": "https://wpnews.pro/news/show-hn-makoto-the-claude-code-plugin-that-keeps-the-ai-honest.md", "text": "https://wpnews.pro/news/show-hn-makoto-the-claude-code-plugin-that-keeps-the-ai-honest.txt", "jsonld": "https://wpnews.pro/news/show-hn-makoto-the-claude-code-plugin-that-keeps-the-ai-honest.jsonld"}}