Sandbox
@Clear-Sights/Makoto

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.

45 stars4 forksPythonUpdated 7d ago
Who it's for

Builders who use Claude Code and want their agent’s claims checked against its own session record.

What it delivers

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

  1. 1Run
    /plugin marketplace add Clear-Sights/Makoto
    /plugin install makoto@makoto
  2. 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
  3. 3Run
    pip install -e /path/to/makoto
    # Then add makoto hook entries to ~/.claude/settings.json manually — see "Manual wiring" below.

README

Makoto

CI

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_eligible is 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_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 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 matched
  • content.illusory_interruption_claim a fabricated "interrupted by user" claim with no genuine harness-set interruption anywhere in this session's recorded history

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)
  • 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. 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 idOne-line triggerFireCertification
gate.completion"done / created X" but the artifact isn't on diskblockingestablished
gate.advanceadvancing a phase whose precondition isn't recorded as metblockingestablished
gate.green_claim"suite green" against a recorded test failureblockingestablished
gate.droppedan identifying forward promise left undischarged at turn-endblockingestablished
gate.fabricated_action"I ran X" in a turn with no tool call at allblockingestablished
gate.named_test"test_foo passes" against a recorded FAILED of that named testblockingestablished
gate.stale_pass"all tests pass" against pytest's own live lastfailed recordblockingestablished
gate.claimed_running"it's running/up" contradicted by this session's own Bash recordblockingestablished
gate.run_promisedlast turn promised a run ("I'll run the tests") and no Bash call followedblockingestablished
gate.claimed_shipped"merged/pushed/live" with no successful remote-mutating call on recordblockingestablished
gate.claimed_consent_absentcites the operator's approval, instruction or word in a session whose transcript carries no genuine operator turn at allblockingnew
gate.unexamined_wallstates that a fact cannot be determined when no action at all has been taken since the operator's last turnblockingnew
gate.livenessa statement with no live effect inside a closed functionblockingestablished
gate.hollow_testa test gutted so it can never fail (no assert, tautology, swallowed failure, uncollectable)blockingestablished
gate.canonlast call ended in an unresolved direct error, or a byte-identical stuck retry loopblockingreplayed
gate.canon_fingerprintsported canon fingerprints in the robust core established by gold-oracle certificationblockingestablished
gate.contract_orderturn ends with a declared Plan's dependency remainder non-emptyblockingestablished
gate.self_wiredmakoto's own hook wiring partially stripped from settings.jsonadvisoryadvisory
gate.canon_fingerprints_advisorythe advisory remainder (soft/claim atoms or gold-disqualified)advisoryadvisory
gate.relative_path_citationa chat response citing a non-absolute (unclickable) pathadvisoryadvisory
gate.plan_item_driftopen plan/task-labeled commitments sourced from chat proseadvisoryadvisory

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.

EngineJudgesOne line
Wardthe pending actnothing outright bad happens
Keelthe sequencea session neither capsizes nor gets lost
Makoto (this repo)the statementwords 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.

OutcomeObserved mechanismProcess exit
clean PreToolUse callno blocking decision0
error-level pre-check findingstdout JSON hookSpecificOutput.permissionDecision='deny'0
Stop-gate findingstdout JSON decision='block'0
invalid/non-object payloadno blocking decision2

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

Repository payload18 top-level entries
  • .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 it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More hooks

CLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies

80k

Warcraft III Peon voice notifications (+ more!) for Claude Code, Codex, IDEs, and any AI agent. Stop babysitting your terminal. Employ a Peon today.

5k
bahni-m/
code-with-quran

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.

48
zachahn/
vomit

Clean up Claude's token vomit with a separate LLM. Save your tokens, Opus is hopeless

193

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.

1.5k