When Claude says “tests pass”, makoto checks the record — every claim held against the agent's own logged deeds; fakes blocked, not warned. Each check ships at measured zero false positives. 誠
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 in makoto/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 wroteSecurity-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 to AutoAddPolicy · content.timing_unsafe_compare timing-unsafe == on a secret/HMAC/digestSelf-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 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 / created X" 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 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-keysgate.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.
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 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.
# 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 + 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 # Then add makoto hook entries to ~/.claude/settings.json manually; see "Manual wiring" below.
The state dir and makoto.db are created lazily on the first hook invocation; there is no separate
init step.
# Plugin install path: /plugin uninstall makoto # Non-plugin / legacy settings.json path: 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.