CLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies
Claude Code integrity hooks for recorded claims
Makoto adds Claude Code hooks that compare what the agent says it did against what the session record actually shows. If a claim has no matching tool call, test run, commit, or other recorded deed, the hook blocks the action or the turn and returns a short correction.
Builders who use Claude Code and want their agent’s claims checked against its own session record.
You can stop fake test, commit, or verification claims from slipping through as if they were real.
What it does
Pre-tool and stop-time checks
Runs on `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionStart` hook events.
Claim-to-record matching
Checks that statements like tests passing, code being committed, or a certificate being verified are backed by the logged session record.
Blocking and advisory gates
Separates blocking gates from advisory ones, with specific checks such as `gate.stale_pass`, `gate.claimed_running`, and `gate.relative_path_citation`.
Plugin install wiring
Installs as a Claude Code plugin without rewriting `~/.claude/settings.json` manually.
Replay-tested dispatcher
Includes evaluation and tests that replay authored sessions and verify gate behavior.
How to get it
- 1Run
/plugin marketplace add Clear-Sights/Makoto /plugin install makoto@makoto
- 2If you previously ran the old python -m makoto install (0.3.0 or earlier), your…
python -m makoto uninstall # removes old settings.json entries /plugin install https://github.com/Clear-Sights/Makoto # installs the plugin
- 3Run
pip install -e /path/to/makoto # Then add makoto hook entries to ~/.claude/settings.json manually — see "Manual wiring" below.
README
Makoto
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.
That publication claim is deliberately bounded: Shipped plugin — installable and versioned. The dispatcher is replay-tested against authored sessions; its effect on a live session's outcome is unmeasured.
Integrity, as this tool uses the word, is exactly that agreement: a claim the agent made this
turn is matched by the record of the deed it names. Nothing wider — not correctness, not code
quality, not whether the deed was a good idea. So gate.relative_path_citation says a bare
path is "a communication-quality signal, not an integrity violation": it contradicts no claim
against the record, it is only harder to follow. makoto.vocab's _INTEG_VOCAB (vocab.py) is
the lexical half of the same idea — the word-set naming integrity concepts in a subject's
code — and is not a second definition of this one.
Six words this page reuses. None has a single owner, so each is listed with every artifact
that produces it. Blocking is an outcome -- the act is stopped -- and these artifacts produce it. NO COUNT is
stated for this list or the next: a number in prose that nothing checks drifts from the list
beside it, which is how the advisory list came to say seven while naming eight, and how
this list said five while the paragraph below names a sixth.
verdict.BLOCK is the raw one a check returns; verdict.apply folds it by posture and
dispatch.py turns the result into a Pre-edge permissionDecision: "deny" or a Stop-edge
{"decision": "block"}. registry.POSTURE_BLOCK is a check's own declared tier, an input to
that folding and not the same vocabulary. registry.blocking_eligible (Stop edge, may_block,
posture BLOCK) decides which end-of-turn gates the count above calls blocking, and is False for
every pre-check even though pre-checks deny. configchange.py emits a block of its own from
outside the check registry, so no registry predicate is defined over it. And "blocking robust
core" below counts canon fingerprints in _canonAtoms.BLOCK_IDS, a population of patterns.
Advisory names these things. Two are columns of the catalog table: the Certification label, and
the Fire column -- which is a two-valued TIER label reading blocking or advisory, not a print
of Finding.level, whose values are error and advisory; that column is therefore also a
producer of the word blocking above. Two are check tiers: registry.POSTURE_ADVISE, a
check's declared tier, and membership of registry._ADVISORY_ALLOWLIST -- blocking_eligible
consults the posture, not the allowlist, and registry.py warns in its own words that the two
agree only because today's four happen to coincide. One is a raw outcome, verdict.ADVISE. One
is gate.configchange_advisory, which fires without blocking from outside the registry, so it is
in neither the allowlist nor the count. One is the canon "advisory remainder" of fingerprints
resting on a soft or disqualified atom. The last is Finding.level == "advisory", which the Fire
column reports but is not -- checks emit it directly, and dispatch.py's _OUTCOME_FOR_LEVEL
maps it to verdict.ADVISE.
Silent has five senses and they sit on different axes, which is why one of them looked like a
contradiction. A fail-open is never silent: dispatch.py emits its notice whenever a check
faulted, gated on the fault and never on posture. A check "staying silent" emitted no finding.
verdict.SILENT is a configured posture that softens outcomes toward ALLOW -- but not
unconditionally: a meta-layer BLOCK still floors at ASK, and it is # record only, so audit rows
are written either way. A verifier "silently neutered" is the SUBJECT's code losing a check,
which is what several patterns exist to catch. And a "silent bypass" is a fifth thing again -- an
override that leaves no record, which is why an allow carries its rationale and why the oversight
clamp is "never overridden SILENTLY".
A claim is one of the three ledger row kinds named above (verdict, certified-fact,
testrun); where this page says the agent "claims" something it means the utterance a row may
later record; and where it says "the claim is", the page is asserting something itself. A check
in a pattern description is one in the subject's code; a check of makoto's own is a
registry.Check. The record has six referents: the session's recorded call stream; the RECORD
step of the receipt chain, an appended kind="audit" row; the written logs audit.jsonl and
dispatch_errors.jsonl, which outlive the session; a FOREIGN ledger, as in gate.stale_pass
reading pytest's own lastfailed; and "on the record" for a makoto-allow: rationale left in
the subject's source. The durable store the word is named after is makoto.record.db, the SQLite
file under the state dir -- a sixth referent, and the one several of the others are checked
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.
Both of those words are literal here, and the thing they name ships. The tender is the receipt
(makoto.state.ledger.emit_receipt, and the Receipt section below): a read-time view over the
chain, never persisted, listing each claim with its own row_index and row_hash. A claim is
spendable when it is trace_bound -- at or before verified_through's cut, so the chain from
the claim to that point verifies -- and only three row kinds are claims at all: verdict,
certified-fact and testrun, the kinds that assert something about the world. Records of deeds
and machinery (audit, touched, release.operator, fetch, exemption) are not claims and are
counted separately. "Without re-deriving it" is what the row hash buys: the reviewer re-checks a
citation instead of re-running the work. A claim after the first broken link is still listed,
undisguised, and is not counted as trace-bound -- it is exactly the not-spendable case.
What it catches
makoto fires on mechanical hook events — every PreToolUse, PostToolUse, and Stop — and
blocks on pre-check findings and blocking end-of-turn gate findings. The live inventory is:
- 15 pre-checks (every one denies the tool call;
blocking_eligibleis about the Stop edge and is False for all of them) - Pre-check ids grouped by dotted prefix —
content: 12,event: 2,gate: 1 - 23 Stop checks (all checks registered at the Stop edge)
- 21 end-of-turn gates (
may_block=True) - 17 blocking end-of-turn gates (
registry.blocking_eligible) - 4 advisory end-of-turn gates (advisory-allowlisted)
Two different things are called a gate in that list, and the counts are not comparable. The
gate. in a pre-check id is a naming prefix and nothing more; an end-of-turn gate is a
check registered at the Stop edge with may_block=True. The one pre-check carrying the prefix,
gate.contract_order, is not an end-of-turn gate — it has a same-named Stop sibling that is, and
the two are separate checks with separate predicates. Every count above is scoped by edge, so no
check is counted twice within a line.
Every pre-check and every non-advisory end-of-turn gate blocks; there is no silent "warning" tier
for those (see Fire level) — the documented exceptions are
gate.self_wired, gate.canon_fingerprints_advisory, gate.relative_path_citation, and
gate.plan_item_drift (below), advisory-only checks that by design never block.
Verifier weakening — a check silently neutered
content.verifier_predicate_weakenedloose-comparator verifier (startswith/endswith/re.matchwhere==is meant)content.verifier_exit_maskingexit-code masking (|| true,; true,set +eon a test/build/lint)content.verifier_body_hollowedhollowed verifier body (return True/passin a constitution check)content.env_gated_auditaudit/verification code gated behind an env var ·content.integrity_suppression_flagintegrity-named suppression flag (*_skip = true)
Fabricated evidence — a claim with no backing artifact
content.phantom_citationphantom citation (Author-Year not inmakoto/docs/CITATIONS.md)content.unsourced_webfetchWebFetch of a URL never seen in any prior tool result this sessioncontent.fabricated_commit_shafabricated commit SHA/tag presented as proof of a commitcontent.deferred_checkbox_theaterDEFERRED-style checkbox theater on an open to-do itemcontent.illusory_authorship_traileran illusory Claude/Anthropic authorship or generation attribution (trailer, session link, routing address, or "Generated with/by Claude" footer) — a plain "Claude Code" product-name mention is not matchedcontent.illusory_interruption_claima fabricated "interrupted by user" claim with no genuine harness-set interruption anywhere in this session's recorded history
Self-defense
content.self_mute_guardmakoto 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_reverta whole-file Write that reverts a file to an earlier byte-identical content after an intervening different Write (A→B→A, no net progress)gate.contract_ordera 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. The table below is the summary; the long-form description of every gate is in docs/CATALOG.md (relocated from this section, word for word).
The certification column uses the following labels, each naming its own denominator:
- established — certified at zero false positives on the named negative sets: the shipped
corpus for the ordinary blocking gates (the warning-tier-elimination invariant below — a
pattern either blocks at proven zero corpus-FP, or it is cut), and additionally the
planted-clean and real-Claude-gold negative sets for
gate.canon_fingerprints(gold-oracle certification). Zero-FP on those sets is the claim; the live-session false-positive rate accumulates from field use and is not covered by it. - replayed — a corpus replay ran but is inconclusive by the gate's own admission (the honest corpus almost never carries the triggering precondition), so certification rests instead on held-out adversarial RED fixtures plus that near-vacuous corpus-FP check.
- advisory — uncertifiable by design or not yet corpus-measured; recorded to the audit log, never emitted as a block decision.
| Check id | One-line trigger | Fire | Certification |
|---|---|---|---|
gate.completion | "done / created X" but the artifact isn't on disk | blocking | established |
gate.advance | advancing a phase whose precondition isn't recorded as met | blocking | established |
gate.green_claim | "suite green" against a recorded test failure | blocking | established |
gate.dropped | an identifying forward promise left undischarged at turn-end | blocking | established |
gate.fabricated_action | "I ran X" in a turn with no tool call at all | blocking | established |
gate.named_test | "test_foo passes" against a recorded FAILED of that named test | blocking | established |
gate.stale_pass | "all tests pass" against pytest's own live lastfailed record | blocking | established |
gate.claimed_running | "it's running/up" contradicted by this session's own Bash record | blocking | established |
gate.run_promised | last turn promised a run ("I'll run the tests") and no Bash call followed | blocking | established |
gate.claimed_shipped | "merged/pushed/live" with no successful remote-mutating call on record | blocking | established |
gate.claimed_consent_absent | cites the operator's approval, instruction or word in a session whose transcript carries no genuine operator turn at all | blocking | new |
gate.unexamined_wall | states that a fact cannot be determined when no action at all has been taken since the operator's last turn | blocking | new |
gate.liveness | a statement with no live effect inside a closed function | blocking | established |
gate.hollow_test | a test gutted so it can never fail (no assert, tautology, swallowed failure, uncollectable) | blocking | established |
gate.canon | last call ended in an unresolved direct error, or a byte-identical stuck retry loop | blocking | replayed |
gate.canon_fingerprints | ported canon fingerprints in the robust core established by gold-oracle certification | blocking | established |
gate.contract_order | turn ends with a declared Plan's dependency remainder non-empty | blocking | established |
gate.self_wired | makoto's own hook wiring partially stripped from settings.json | advisory | advisory |
gate.canon_fingerprints_advisory | the advisory remainder (soft/claim atoms or gold-disqualified) | advisory | advisory |
gate.relative_path_citation | a chat response citing a non-absolute (unclickable) path | advisory | advisory |
gate.plan_item_drift | open plan/task-labeled commitments sourced from chat prose | advisory | advisory |
Inspect the live catalog with makoto pattern list; see one pattern in full with makoto pattern show content.phantom_citation.
- blocking robust core: 4 of 17 ported canon fingerprints
- advisory remainder: 13 ported canon fingerprints
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 <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.
Legitimately writing a flagged shape?
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()
The constitution every pattern derives from is 誠 (makoto): a word is real the way water is wet —
a constitutive property, not an after-the-fact audit. (An internal design document elaborating
this is not shipped in this repository, so it is deliberately not cited here; every normative
statement a pattern rests on appears self-contained in this README, docs/CATALOG.md,
or the pattern's own makoto pattern show output.)
Install (plugin)
/plugin marketplace add Clear-Sights/Makoto
/plugin install makoto@makoto
Enabling the plugin is the whole install: .claude-plugin/plugin.json + hooks/hooks.json
auto-wire dispatch on enable. Claude Code registers PreToolUse, PostToolUse, Stop,
SubagentStop, and SessionStart hooks pointing at ${CLAUDE_PLUGIN_ROOT}/makoto/_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.record.db are created lazily on the first hook invocation.
Companion setting (optional): suppress the harness auto-trailer
An illusory AI-authorship commit trailer can reach a commit through either path. 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.
Migration from 0.3.0
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. How to tell if you're affected: grep makoto ~/.claude/settings.json — any hit
means the old entries are present. Migrate cleanly:
python -m makoto uninstall # removes old settings.json entries
/plugin install https://github.com/Clear-Sights/Makoto # installs the plugin
Contributing
Reports are welcome and are credited by name; pull requests from outside this repository are not merged. See CONTRIBUTING.md for why, and for what to send instead.
Siblings
Makoto owns the statement surface alongside the independently installed engines for act and
sequence. None inherits or implies the others' coverage. The marketplace inventory is owned by
Courthouse:
claude plugin marketplace add Clear-Sights/Courthouse.
| Engine | Judges | One line |
|---|---|---|
| Ward | the pending act | nothing outright bad happens |
| Keel | the sequence | a session neither capsizes nor gets lost |
| Makoto (this repo) | the statement | words aren't empty |
Non-plugin install (power users)
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.record.db are created lazily on the first hook invocation; there is no separate
init step.
Uninstall
# Plugin install path:
/plugin uninstall makoto
# Non-plugin 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.record.db
remain for forensic value. To fully reset, rm -rf the dir.
CLI
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 # non-plugin: wire settings.json directly (prefer the plugin)
python -m makoto uninstall # remove makoto-managed settings.json entries
Manual wiring (fallback)
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.
Dispatcher outcomes
The shipped shim communicates findings using the measured response fields below. Invalid input is the distinct process-error path.
| Outcome | Observed mechanism | Process exit |
|---|---|---|
| clean PreToolUse call | no blocking decision | 0 |
| error-level pre-check finding | stdout JSON hookSpecificOutput.permissionDecision='deny' | 0 |
| Stop-gate finding | stdout JSON decision='block' | 0 |
| invalid/non-object payload | no blocking decision | 2 |
Fire level
Every non-advisory live pattern blocks through the dispatcher mechanism measured above. makoto
deliberately has no non-blocking tier: no resting state a check can be assigned to, no
warning/disabled level to demote a pattern into — witnessing a violation and letting the tool
through as a matter of policy is itself an illusory word, the exact weakening shape makoto exists
to catch. Four checks do fire without blocking, and they are named below; the claim is that no
category admits them, not that the count is zero. There is no predicate that qualifies a check
as advisory — each of the four carries its own dated argument, and a fifth would need its own.
That is deliberate: a rule for admitting advisory checks is the tier, rebuilt. 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 — zero false positives on the shipped corpus and gold
negative sets, the only sets the proof runs over; the live-session false-positive rate accumulates
from field use and is not part of that measurement). This still governs every pre-check and every non-advisory
end-of-turn gate. The invariant is enforced by the suite, not at load: makoto.vocab carries
fire_level on its test-fixture PreCheck shape only -- "by convention -- no longer
runtime-checked here", and not the home of this invariant; the level a live gate emits is
Finding.level. The allowed set lives in
tests/_toml_pattern_fixture.py, with tests/test_stop_gate_level_invariant.py firing every live
gate and asserting the level it emits.
The narrow, explicitly-recorded exceptions: gate.self_wired (2026-07-05),
gate.canon_fingerprints_advisory (SPEC-5 Task 9, DESIGN DECISION 26), gate.relative_path_citation,
and gate.plan_item_drift (both 2026-07-09) fire at level="advisory", not "error", so each is
recorded to the audit log but never emitted as a block decision. None 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
Files in the repo
- .claude-plugin
- .github
- docs
- eval
- plugin
- tests
- tools
- .gitignore
- .gitleaks.toml
- .gitleaksignore
- CHANGELOG.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
- pyproject.toml
- README.md
- uv.lock
- VENDORED.tsv
Discussion (0)
Ask about usage, or say what you built with itSign in to join the discussion.
No comments yet. Be the first to say what this is good for.
More hooks
Warcraft III Peon voice notifications (+ more!) for Claude Code, Codex, IDEs, and any AI agent. Stop babysitting your terminal. Employ a Peon today.
Read the Qur'an while Claude Code works. Start a session with 'claude --cwq' and a reader beside it walks forward through the Qur'an one ayah per prompt, resuming where you left off — in a terminal pane or a browser tab. Zero dependencies, fully offline.
Clean up Claude's token vomit with a separate LLM. Save your tokens, Opus is hopeless
A pre-execution guard for AI coding agents. It blocks destructive Git and file system commands, plus common attempts to access sensitive files, before a tool call runs. Supports Amp Code, Antigravity CLI, Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, Grok Build, Hermes Agent, Kimi Code, OpenClaw, OpenCode, and Pi.
Automated TDD enforcement for Claude Code