Show HN: Makoto the Claude Code plugin that keeps the AI honest
Makoto is an integrity hook for Claude Code that monitors the AI agent's tool calls and blocks those that fake checks, such as claiming to run tests without actually running them, fabricating citations, or disabling security verifiers. It operates on a ledger of the agent's own claims and ensures promises are fulfilled.
Notifications You must be signed in to change notification settings
Fork 3
Star 37
BranchesTags
Open more actions menu
Folders and files
NameName
Last commit message
Last commit date
Latest commit
History
46 Commits
46 Commits
.claude-plugin
.claude-plugin
.github
.github
commands
commands
docs
docs
hooks
hooks
makoto
makoto
tests
tests
.gitignore
.gitignore
.gitleaksignore
.gitleaksignore
CHANGELOG.md
CHANGELOG.md
CODE_OF_CONDUCT.md
CODE_OF_CONDUCT.md
LICENSE
LICENSE
README.md
README.md
pyproject.toml
pyproject.toml
Repository files navigation
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.
What it catches
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 in makoto/docs/CITATIONS.md)
content.unsourced_webfetch WebFetch of a URL never seen in any prior tool result this session
content.fabricated_commit_sha fabricated commit SHA/tag presented as proof of a commit
content.deferred_checkbox_theater DEFERRED-style checkbox theater on an open to-do item
content.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 whitelisted
content.paramiko_host_key_weakened paramiko SSH host-key check weakened to AutoAddPolicy · 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 via settings.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 location
gate.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 / created X" but the artifact isn't on disk
gate.advance: advancing a phase whose precondition isn't recorded as met
gate.green_claim: "suite green" against a recorded test failure
gate.dropped: a forward promise carrying identifying info ("I'll add def X to Y", "3 tests in Z") left undischarged at turn-end, said-but-not-done, checked against the agent's own touched-keys
gate.fabricated_action: "I ran X / executed Y" in a turn with no tool call at all (presence-of-work, not command-text match)
gate.named_test: "test_foo passes" against a recorded FAILED of that exact named test (coreference-pinned; the green_claim delta)
gate.stale_pass: "all tests pass" against pytest's own lastfailed 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 (see Fire level). Partial-strip detection of makoto's own .claude/settings.json hook wiring: fires if PreToolUse/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 the ConfigChange watch section 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-op try/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 a skipif/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 (see ANCESTRY.md §2 Part B for the corrected framing).
gate.canon_fingerprints: blocking. 4 of 17 ported session-level "canon" fingerprints (SPEC-5 Task 9, from REF-lever-graded-primitives/signalminer/grade_planted.py's 27-fingerprint THE_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's ContractOrder 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; see makoto/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's stale_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 the checks/ catalog 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.
Inspect the live catalog with makoto pattern list; see one pattern in full with makoto pattern show content.phantom_citation.
Discharging a permanent session-level block
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 :
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.
Legitimately writing a flagged shape?
Annotate the line with makoto-allow: (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 docs/SPIRIT.md: 誠 (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.
Install (recommended: Claude Code plugin)
In Claude Code:
/plugin install https://github.com/Clear-Sights/Makoto
(or a local clone path)
Claude Code reads hooks/hooks.json and registers PreToolUse, PostToolUse, and Stop hooks pointing at ${CLAUDE_PLUGIN_ROOT}/_dispatch_shim.sh automatically (which execs python -m makoto._dispatch). ~/.claude/settings.json is NOT modified; the plugin system manages its own hook registry.
State dir + ma
[truncated for AI cost control]