Hooks the harness runs for you — not the model. Advisory where a nudge is enough, blocking where a rule must hold 100% of the time.
#scripts/run-fixtures.sh
script
Structural validator for router-decision fixtures: checks shape, gate names, and cross-rules, no Claude run.
Trigger Bash script: bash scripts/run-fixtures.sh (no args). Run in CI / before commit.
What it doesValidates the router-decision fixtures under tests/fixtures/. It is a structural validator, not an agent runner: it asserts each fixture parses as JSON and matches the documented shape, uses only known gate names and actions, and obeys the documented cross-rules. It does NOT run Claude or check rationale text — that is human review.
Examplebash scripts/run-fixtures.sh iterates tests/fixtures/*.json, printing OK <name>.json per valid router-decision fixture (e.g. OK build-happy-path.json, whose expected.next_action is advance_phase and gate_to_record is phase_exit_gate: pass) and finishing with run-fixtures: all fixtures OK.. The abort cross-rule is exercised by failure-stop-triggered.json (next_action: abort, gate_to_record: failure_stop_gate: fail); a fixture that set next_action to abort without that exact gate makes the embedded Python check print FAIL: <path>: next_action=abort requires gate_to_record='failure_stop_gate: fail', got ... to stderr and the script exits 1. (There is no run-fixtures: FAIL: prefix on per-fixture failures — the defined fail() helper is unused; that prefix only appears on the no-fixtures skip and the final all-OK line.) If no fixtures exist it prints run-fixtures: no fixtures under <dir> — skipping. and exits 0.
How it worksFor each JSON fixture a Python check enforces: id matches the filename stem; workflow_type is one of BUILD|DEBUG|REVIEW|PLAN|FIX; required top-level fields description/input/expected are present; every gate in input.gates and expected.gate_to_record is one of the five gates from .claude/references/orchestration-gates.md (plan_trust_gate, phase_exit_gate, failure_stop_gate, memory_sync_gate, skill_precedence_gate); expected.next_action is one of advance_phase|remediate|block|request_engineer|resume_existing|abort; gate results are pending|pass|fail. Cross-rules: next_action==abort requires gate_to_record=='failure_stop_gate: fail'; expected.rationale must be a non-trivial sentence (>=20 chars). Fixtures whose fixture_type is not router-decision are checked only for JSON well-formedness plus id==stem, so handoff/schema fixtures can coexist in the same directory.
Why it worksThe router's decision contract (which gate to record, when to abort, what next action is legal) is easy to drift as fixtures accrue. A deterministic, Claude-free validator catches malformed or self-contradictory fixtures in CI without any API spend, keeping the router's expected behavior pinned.
#scripts/run-evals.sh
script
Runs (or lists) per-skill eval scenarios; read-only by default to avoid surprise API spend.
Trigger Bash script: bash scripts/run-evals.sh [--list | --skill <name> | --eval <path>].
What it doesDiscovers and lists the eval scenarios under evals/, grouped by skill. By default it is read-only: for each scenario it points you at the eval file (which holds the setup and prompt) so an engineer, or a separate Claude session, can run it explicitly. You opt into automation by pointing it at an executor and grader via environment variables.
Examplebash scripts/run-evals.sh --list prints scenarios grouped by skill — e.g. a == fix == block listing [positive] evals/fix/eval-01-real-fix.md with its title Single-file null check fix with reproduction on the next line. bash scripts/run-evals.sh --skill fix walks that skill's eval-*.md files; with EVAL_EXECUTOR unset it stays in manual mode — printing Manual mode. Read the eval file and run the prompt yourself., the eval file path, and where to record .output.md/.verdict.md — which 'avoids surprise API spend'. (--skill names an existing directory under evals/; a name with no such directory, e.g. a nonexistent mtk, prints ERROR: no such skill: mtk and exits 1.)
How it worksModes: --list (default), --skill <name>, --eval <path>, plus --run-id <string> and -h/--help. Results are written under evals/results/${RUN_ID} (RUN_ID defaults to a timestamp). Manual mode (no EVAL_EXECUTOR) prints Manual mode. Read the eval file and run the prompt yourself., the eval file path, the evals/$skill/grader.md grader path, and the record paths for <name>.output.md and <name>.verdict.md — it does not print the prompt text itself. Automated mode extracts the fenced ``prompt block from the scenario file, pipes it to $EVAL_EXECUTOR, and — when $EVAL_GRADER is set and evals/$skill/grader.md` exists — assembles a grader payload (grader.md + eval file + actual output) and records the returned verdict.
Why it worksRouter and skill behavior need behavioral regression checks, but running agents costs money. A read-only-by-default runner lets engineers inspect and deliberately execute scenarios, with an explicit opt-in executor/grader path for automation, so evals never fire API calls by accident.
#scripts/setup-detect.sh
script
Read-only detector for tech stack, package manager, React Native/Expo, and monorepo layout — emits one JSON object.
Trigger Bash script (bash scripts/setup-detect.sh --json); called by setup-bootstrap STEP 0/4.5 and setup-audit STEP 0
What it doesThe single source of truth for repo detection used across setup. It scans marker files to determine the stack(s), picks the package manager by lockfile priority, detects React Native/Expo (including nested workspace apps), and classifies monorepo layout with an enumerated package list. It writes nothing.
Examplebash scripts/setup-detect.sh --json in a pnpm monorepo emits {"stacks":["typescript"],"primary_candidate":"typescript","package_manager":"pnpm","react_native":{"detected":false,"expo":false},"monorepo":{"is_monorepo":true,"ambiguous":false,"signals":["pnpm-workspace.yaml",...],"packages":["apps/web","packages/ui"],"packages_skipped":0},"go_detected":false,"multiple_lockfiles":false,"parse_errors":[]}. The calling skill reads those fields to write .claude/tech-stack and decide monorepo handling.
How it worksPure Python3 (hard-required, exit 2 if absent) over os.walk. Stack markers: *.csproj/*.sln/*.slnx→dotnet, pyproject.toml/setup.py/requirements.txt/Pipfile→python, package.json→typescript, go.mod→go_detected (unsupported). Package-manager priority bun>pnpm>yarn>npm; multiple_lockfiles flags conflicts. Monorepo signals: lerna/pnpm-workspace/turbo/nx/rush/workspaces, csproj≥4, sln≥2, pyproject≥2, or conventional dirs (apps/packages/services/libs) with >1 marked sub-project; ambiguous is true on mixed signals so the skill asks the engineer. Packages enumerated from workspaces globs + csproj/pyproject dirs + conventional dirs, deduped, sorted, capped at 20. A malformed package.json falls back to a scoped dependencies/devDependencies text scan and is listed in parse_errors.
Why it worksInlining detection bash in the skills made it un-testable and prone to drift between bootstrap and audit. One deterministic, read-only script gives both skills identical answers and keeps marker logic in a single place (F2).
#scripts/verify-commands.sh
script
Runs the build/test/format commands bootstrap will publish in CLAUDE.md; reports verified/failed/skipped as JSON.
Trigger Bash script fed name<TAB>command lines on stdin (or --file); called by setup-bootstrap STEP 3.5a command verification
What it doesActually executes the commands bootstrap intends to document (sourced from the tech-stack skill, never from repo content) and reports whether each works. This is how CLAUDE.md's Tech Stack section earns a <!-- verified: ... --> stamp or [UNVERIFIED] annotations instead of guessed commands.
Exampleprintf 'build\tdotnet build\ntest\tdotnet test --list-tests\n' | bash scripts/verify-commands.sh emits {"results":[{"name":"build","command":"dotnet build","status":"verified","detail":""},{"name":"test","command":"dotnet test --list-tests","status":"failed","detail":"exit 1: <first stderr line>"}]}. Bootstrap uses that to mark the failing command [UNVERIFIED] and note it in the STEP 5 report.
How it worksReads name<TAB>command lines; an empty command half → skipped. Each command runs under a portable timeout probe (timeout, else gtimeout, else unbounded with a note) via bash -o pipefail -c, with stdin from /dev/null so it can't steal the loop's input. Exit 124 under the timeout binary → failed: timeout after <N>s; other non-zero → failed: exit <code>: <first non-blank stderr/stdout line>. The script always exits 0 itself (per-command outcomes live in the JSON body); usage errors exit 2. --timeout <secs> defaults to 300. Bootstrap's --no-verify-commands bypasses it entirely.
Why it worksDocumented commands that don't actually run are worse than none — they mislead every future agent. Executing them at generation time, with graceful degradation on a sandboxed runner, keeps CLAUDE.md's commands honest (F7).
#scripts/setup-refresh-plan.sh
script
Read-only per-artifact staleness plan for the refresh/check loop; one row per artifact plus a summary and CI exit code.
Trigger Bash script (--json / --check); the sole staleness source for setup-refresh and the engine behind /mtk-setup --check
What it doesComputes, deterministically, which generated setup artifacts have drifted. It emits one status row per artifact (fresh/stale/missing/unstamped/unknown/present/absent) with a reason, so refresh can scope its work and check can gate CI. It writes nothing to the repo.
Examplebash scripts/setup-refresh-plan.sh prints a markdown table and a summary line like 3 fresh, 1 stale, 0 missing, 1 unstamped/unknown; a stale row might read CLAUDE.md | stale | CLAUDE.md footer stamped v7.20.0, installed v7.25.0. With --check, if any non-informational row is stale/missing it prints run /mtk-setup --refresh to reconcile to stderr and exits 1.
How it worksRuns from the git top-level (exit 2 if not a git repo). Seven rows: architecture-principles.md and conventions.md via audit-drift-check.sh --json (drift→stale, no stamp→unstamped); CLAUDE.md via footer-version-vs-.claude/mtk-version.json plus a dependency rescan cross-referencing backtick tokens against package.json/*.csproj/pyproject.toml; detected-tools.json via a 7-day mtime TTL; AGENTS.md by checking the Auto-generated by MTK marker then regenerating into a temp file and diffing; path-references via verify-references.sh (exit 3→stale, absent→row omitted); setup-answers.json informational only. Sibling helpers resolve via SCRIPT_DIR so it works from a plugin cache; a missing helper degrades that row to unknown rather than failing.
Why it worksRefresh must not re-derive staleness ad hoc, and CI needs a stable exit code. One read-only plan script gives both a single, testable verdict — and degrades gracefully when run from a target repo that never received the helper scripts.
#Confidence tags + verify-claims.sh grounding loop
concept
Every generated claim carries an evidence-graded tag; verify-claims.sh greps each anchor and downgrades failures.
Trigger Enforced by setup-audit STEP 3.7 / setup-bootstrap STEP 3.5a; consumed by setup-converge and setup-refresh
What it doesMTK's evidence-first mechanism. Descriptive principle tags ([EXTRACTED]/[INFERRED:N]/[AMBIGUOUS]) say what the code does; prescriptive rule tags ([ENFORCED]/[CONVENTION]/[ASPIRATIONAL]) say what reviewers should do. A deterministic verifier re-greps every cited anchor and downgrades zero-hit claims in place, then surfaces the weak ones instead of hiding them.
ExampleAn audit writes [EXTRACTED] Uses EF Core. Evidence: \Program.cs:61 UseSqlServer\. If a refactor removed that call, bash scripts/verify-claims.sh .claude/references/architecture-principles.md finds zero hits and rewrites the line to [INFERRED:0.5 unverified], records it in .claude/.mtk-cache/weak-claims-architecture-principles_md.json, and the audit prints ⚠️ Weak claims surfaced: N. setup-converge later reads that original tag to grade the drift blocking/flag/note.
How it worksTags require a real anchor (file:line, glob+hit-count, or symbol). verify-claims.sh parses tagged lines, grep/AST-checks each anchor against the working tree, downgrades [EXTRACTED]→[INFERRED:0.5 unverified] and [ENFORCED]→[ASPIRATIONAL unverified] in place, and writes per-doc weak-claims-<doc>.json (full) + .md (top-5, paste-ready). A one-pass retry loop re-derives or deletes (only self-generated) failing lines, then re-verifies once — never re-upgrading a tag without a resolving anchor. It also runs a transient-state lint and terminology denylist. The scheme is codified in audit-grounding.md.
Why it worksLLM audits confidently assert things that aren't true (dead imports as live capabilities, single examples as laws). Grading by evidence and mechanically falsifying each anchor is what makes MTK's claim-verification distinctive — a passing audit surfaces its weak claims rather than pretending they don't exist.
#hooks/spec-approval-trigger.sh
hook
PostToolUse hook that queues planning when a spec transitions to approved, and re-queues approval when a sealed artifact is edited after approval.
Trigger Automatic PostToolUse hook on Edit|Write (wired in .claude/settings.json, timeout 5). Tier-2 (skill-invoking); respects MTK_HOOKS_TIER2 via mtk_queue_enabled.
What it doesAfter an edit to a spec or plan, this hook does two things. First, if an active workflow sealed that file at approval and its bytes no longer match, it flags the approval as stale and queues the planning skill to re-approve. Second, when a spec markdown file transitions to 'status: approved' (and was not already approved at HEAD), it queues planning-and-task-breakdown so the next turn moves into task breakdown. It is advisory (exit 0) — it never blocks; it defers work to a queue.
ExampleThe engineer flips docs/specs/2026-07-09-refund-flow.md to - Status: approved and saves. The hook confirms this is a real transition (HEAD did not say approved), then queues a skill. Next prompt, the dispatcher surfaces: planning-and-task-breakdown, reason spec docs/specs/2026-07-09-refund-flow.md transitioned to status: approved. If instead an already-sealed spec is edited after approval, it queues with reason approval seal STALE — ... re-approve at Phase 2.5 before continuing.
How it worksGated by mtk_queue_enabled (the MTK_HOOKS_TIER2 kill-switch). Fires only for docs/specs/*.md, docs/plans/*.md, or tasks/todo.md. Stale-seal path: scans .mtk/workflows/*.json (via python3) for active workflows whose results.approval_seal lists this file, runs bash scripts/workflow-artifact.sh verify-seal <uuid>, and on rc=1 (stale) calls queue_skill (from lib/skill-queue.sh) with source_hook/dedup key spec-approval-seal-stale-<uuid>. Approved-transition path (spec-only): matches the approved marker as a bold list item (- Status: approved) or YAML frontmatter (status: approved), compares against the HEAD version to confirm a true transition, then queues planning-and-task-breakdown with source_hook/dedup key spec-approval-trigger-<slug>.
Why it worksApproval is the gate between planning and building; a human flipping a status line shouldn't require the agent to remember to advance. Auto-queuing planning makes the workflow move forward deterministically. The seal check closes a subtler hole: if an approved spec/plan is quietly edited after sign-off, the earlier approval no longer describes reality, so the hook forces re-approval rather than letting stale approval carry.
#.claude/references/orchestration-gates.md
reference
The five fail-closed gates the implement workflow records before advancing; plan_trust_gate governs approval.
Trigger Loaded on-demand by the implement workflow and orchestration skills; gate decisions recorded via scripts/workflow-artifact.sh gate
What it doesDefines the only five places /mtk implement may advance, retry, or stop: plan_trust_gate, phase_exit_gate, failure_stop_gate, memory_sync_gate, and skill_precedence_gate. Every gate decision is persisted on the workflow artifact. Gates are fail-closed: pending means "not evaluated yet" and only pass permits the next phase. For this subsystem, plan_trust_gate is the Phase 2.5 spec/plan/todo approval gate.
ExampleAt Phase 2.5 the engineer chooses "Approve & run until done"; the orchestrator records scripts/workflow-artifact.sh gate "$MTK_WF_UUID" plan_trust_gate pass --reason "approve & run until done", which writes gates.plan_trust_gate in {uuid}.json and appends a gate_decided event to {uuid}.events.jsonl. Choosing "Revise" records fail and returns to Phase 1; "Edit first" leaves the gate pending.
How it worksEach gate carries pass/fail/pending semantics and a responsible skill. plan_trust_gate passes only if the engineer approved (Approve & run until done / Approve interactive), the spec+plan+todo trio is all on disk, and no open decisions remain. The workflow-artifact.sh gate helper writes gates.{name} in {uuid}.json AND appends a gate_decided event — reading either surface alone is incomplete. Advancing a phase without recording its gate is treated the same as advancing on fail; a missing gate event may be treated as drift by validators.
Why it worksFail-closed gates make "have we actually approved this?" a deterministic, audited fact rather than a vibe. Because both the state and the event are written, an auditor can reconstruct exactly why the workflow advanced.
#approval seal (scripts/workflow-artifact.sh seal / verify-seal)
concept
SHA-256 seal over approved spec/plan bytes so editing an approved scope artifact invalidates approval.
Trigger Recorded automatically by the implement Phase 2.5 gate on approval; checked by verification-before-completion and the spec-approval-trigger.sh hook
What it doesOn approval, the implement gate records a SHA-256 approval seal over the approved spec/plan bodies (the todo is progress state and is not sealed). Because the seal binds the exact approved bytes, editing an approved artifact afterward invalidates the approval deterministically — a STALE seal blocks completion and re-opens the Phase 2.5 gate, rather than trusting a stale status: approved marker.
ExampleA spec is approved and sealed. Later a success criterion is edited. verification-before-completion runs verify-seal, sees STALE, and refuses to accept the completion claim; independently, the spec-approval-trigger.sh hook detects the byte mismatch and re-queues the approval step. Moving a goalpost post-approval therefore requires re-opening Phase 2.5 — the seal will not match.
How it worksscripts/workflow-artifact.sh seal records the seal (files + hashes) under results.approval_seal on the workflow artifact and emits an approval_sealed event. verify-seal returns rc 1 on mismatch (stale, printing sealed=/current=), 0 on match, 2 uncheckable, 3 no-seal. verification-before-completion refuses completion on a STALE seal; the spec-approval-trigger.sh hook fires its advisory only on rc 1 and ignores 0/2/3.
Why it worksA status: approved marker is trivially staleable by editing the file after approval. Binding approval to exact bytes makes tampering detectable and deterministic, enforcing the "what was approved is what ships" contract instead of a self-asserted status.
#hooks/verify-completion + hooks/context-budget.sh (Stop hook)
hook
The runtime guardrail behind verification-before-completion: checks completion claims cite fresh command output.
Trigger Automatic — a Stop hook fires when the agent finishes responding; freshness/re-arm tracking is wired in settings.json.
What it doesThe deterministic backstop that makes the verification-before-completion contract enforceable rather than merely documented. Hooks track the latest edit and latest verification in the session and warn when a 'done' claim is stale, missing evidence, or based on criteria that got re-armed by a later edit.
ExampleThe agent edits a file, then says 'the fix is complete' without rerunning tests. Because last_edit_seq > last_verification_seq, hooks/verify-completion emits VERIFICATION GAP: criteria re-armed — all success criteria reset to re-armed and the completion claim is rejected until verification reruns after the edit.
How it workshooks/context-budget.sh records the latest edit time and the latest verification command run in the session; hooks/verify-completion compares those timestamps/sequence numbers and warns with VERIFICATION GAP: when evidence is stale or missing, and with VERIFICATION GAP: criteria re-armed when an edit landed after the last verification. The Stop hook is the actual enforcement mechanism — the skill text is the contract, the hook is the guardrail.
Why it worksAn agent has every incentive to declare success; a documented contract alone can be rationalized past. Comparing edit-vs-verification event order deterministically catches the exact 'I ran the tests a few minutes ago (before I touched the code)' staleness that prose review misses.
#command-verification.md (F7)
reference
Runs the build/test/format commands MTK is about to publish and annotates any that fail as [UNVERIFIED].
Trigger Read on-demand by setup-bootstrap STEP 3.5a, unless --no-verify-commands was passed.
What it doesThe F7 command-verification procedure: before writing a repo's Tech Stack section into CLAUDE.md, it actually runs the commands it's about to advertise and applies verified/failed/skipped/unavailable outcomes — so the generated docs never promise a command that doesn't work. (It sits in the broader verification family; it runs at setup time, not inside the implement loop.)
ExampleDuring bootstrap, printf 'build\t%s\ntest\t%s\nformat\t%s\n' ... | bash scripts/verify-commands.sh --timeout 300 runs dotnet build, dotnet test --list-tests, and dotnet format --verify-no-changes. Build passes, format fails — so CLAUDE.md gets <!-- verified: build ✓ (2026-07-03) --> and the format line is written with [UNVERIFIED — <detail>] appended. A failed command is never dropped and never appears unannotated.
How it worksAssembles the three commands (test uses the stack's list/collect-only variant; format uses check-mode, not the mutating form used by hooks/format-on-edit.sh), pipes them as name<TAB>command lines to scripts/verify-commands.sh. A tree-mutation guard snapshots git status --porcelain before and git checkout -- any tracked file the verification run dirtied. Outcomes: verified → normal line + a single <!-- verified: ... --> stamp; failed → line kept + [UNVERIFIED — ...]; skipped → line kept, noted in the report; verifier unavailable (missing/exit 127) → every command annotated [UNVERIFIED — verify-commands.sh unavailable]. Non-interactive runs never block on a failing command.
Why it worksSetup docs that advertise a broken build/test command send every future session down a dead end. Verifying before publishing — and never letting a failure appear unannotated — keeps the generated CLAUDE.md trustworthy even when the verifier itself is absent.
#Sycophancy Index (π) — learnings.sh metrics
concept
A computed metric — approved / (approved + modified + rejected) — that flags when the agent's proposals are being rubber-stamped.
Trigger bash scripts/learnings.sh metrics (aggregates last 30 days; --window-days N); surfaced by toolkit-health
What it doesA single number derived from the decision_origin of captured lessons that estimates how often the agent's recommendations are accepted unchanged versus modified or rejected. A high value suggests the engineer is rubber-stamping proposals (a sycophancy risk); it excludes decisions that weren't model recommendations at all.
Examplebash scripts/learnings.sh metrics returns totals over 30 days and "pi": 0.667 with "warn_threshold": 0.70 and "status": "ok". If accepted-unchanged proposals climb so π reaches 0.70, status flips to warn, and toolkit-health surfaces it as a signal that recommendations may be getting insufficient scrutiny.
How it workslearnings.sh metrics aggregates decision_origin across a window (default 30 days, --window-days) and computes pi = approved / (approved + modified + rejected), using claude-recommended-approved/-modified/-rejected. It excludes user-directed and system-inferred because those aren't model recommendations. status: "warn" when pi >= warn_threshold (default 0.70, tunable via .claude/review-config.json → sycophancy_index.warn_threshold). This is why --decision-origin is a required enum captured on every lesson.
Why it worksIf the agent's suggestions are almost always accepted verbatim, the human review that MTK depends on has quietly stopped happening — a measurable failure mode. Computing π from provenance that's already captured on every lesson turns that risk into an observable metric instead of a vibe.
#scripts/learnings.sh
script
The deterministic engine behind the two-tier lessons store — appends structured entries, rebuilds the markdown, and answers retrieval queries.
Trigger Bash script with subcommands: add, query, metrics, regen-markdown, migrate, list — called by the capture/promote skills and at spec/fix start
What it doesThis is the single write/read path for structured learnings. add allocates a monotonic id (L-YYYY-MM-DD-NNN), enforces the required --decision-origin enum, and appends a JSONL entry; regen-markdown rebuilds tasks/lessons.md from the store; query runs the 5-layer retrieval filter; metrics computes the sycophancy index; migrate back-fills the store from existing markdown; list dumps id+title for diagnostics. It is pure-bash so it can append and grep without an external JSON parser (per S3.3).
ExampleAt the start of a fix, a workflow skill runs bash scripts/learnings.sh query to pull the top 10 relevant lessons for the files about to change. When a lesson is captured, learnings.sh add … --decision-origin claude-recommended-rejected … appends one JSONL line to .mtk/learnings.jsonl, then learnings.sh regen-markdown re-emits tasks/lessons.md. Regenerating is protected by a shrink-guard (hooks/lib/shrink-guard.sh) that refuses a rewrite shrinking the file below 50% of its bytes or 80% of its lines unless MTK_SHRINK_GUARD_OVERRIDE=1 is set (which just emits a warning).
How it worksadd validates --decision-origin against its enum — required for capture sources (correction/promotion/golden-path/review-finding), auto-set to system-inferred for manual/migrate/incident — accepts free-text --source as pass-through (so golden-path is valid without a parser change), and guards only the outer shape of the optional contract fields (--output-contract must be {...}, --prefinal-checklist must be [...]). query applies the 5-layer filter (proximity/recurrence/severity/validity/superseded/phase), returns top 10 (--max N), and takes --scope team|personal|all (default all). metrics aggregates decision_origin over a window (--window-days). migrate parses any prior tasks/lessons.md format into source: "manual", scope: "team" entries, and is idempotent — a re-run is a no-op once the auto-generated store is seeded. Replacing entries goes through mtk_guarded_write to avoid truncation.
Why it worksSkills alone can't guarantee a consistent, dedup-safe, non-truncating write to institutional memory; a single deterministic engine enforces the schema, allocates stable ids, keeps the committed markdown and machine store in sync, and makes retrieval reproducible — turning "lessons" from prose into queryable, ranked context.
#scripts/pr-review-mine.sh
script
Fetches merged PR review threads via gh and clusters repeated reviewer-feedback phrases, fail-soft and suggest-only.
Trigger Bash script: bash scripts/pr-review-mine.sh --prs 10 (also called by repo-health and setup-audit --mine-prs)
What it doesThe deterministic miner under the pr-review-mining skill. It reads PR-level review summaries and per-line review comments from merged PRs (not issue comments, which are status chatter), finds phrases starting with an imperative-ish verb, and reports those whose 4-word prefix recurs across ≥2 distinct merged PRs. It emits markdown by default or JSON with --json, and is fail-soft: when gh (or jq) is missing, or gh is unauthenticated, it emits status: skipped with phrases: [] instead of blocking or guessing.
Examplebash scripts/pr-review-mine.sh --prs 10 --json returns { "status": "ok", "scanned_prs": 10, "phrases": [ { "phrase": "add a regression test", "count": 4, "prs": [21, 22, 23] } ] }. Boilerplate like lgtm, nit, looks good, +1 is filtered by the ## Denylist in the patterns reference, which the script reads at runtime.
How it worksA phrase becomes a candidate when it starts with a verb from IMPERATIVE_HINTS (add, remove, use, rename, prefer, avoid, …), its 4-word prefix occurs in ≥2 distinct merged PRs, and it is not on the denylist. The denylist under the ## Denylist heading of .claude/references/pr-mining-patterns.md is read at runtime (until the next ## heading), so teams suppress noise by editing that file. Output is always tagged [MINED:feedback] and never auto-edits architecture-principles.md; exit is 0 unless argument validation fails.
Why it worksReviewer feedback is a rich, underused signal for what should become a written convention, but naively scraping it would surface boilerplate and single-reviewer opinions; requiring an imperative prefix plus ≥2 distinct PRs, a runtime denylist, and fail-soft behavior keeps the output actionable and never fabricated.
#5-Layer Retrieval Filter
concept
Ranks and filters stored lessons by proximity, recurrence, severity, validity/supersession, and phase so only relevant ones load.
Trigger Run by learnings.sh query at the start of spec-driven-development and fix; returns ranked entries (default top 10, --max N)
What it doesThe scoring-and-filtering pass that decides which of potentially many stored lessons actually surface at the start of a spec or fix. Rather than dumping every lesson into context, it scores entries by how close they are to the current change and how important they are, and hard-drops entries that are expired, superseded, or out-of-phase. Default is the top 10, overridable with --max N.
ExampleStarting a fix that touches scripts/learnings.sh, learnings.sh query scores a lesson whose files include that path +30 (proximity), adds +20 because its recurrence.count >= 2, adds +25 because it's block severity, keeps it because it isn't expired or superseded and its phase matches — so it ranks near the top of the returned set, while a stale expired == true lesson is dropped entirely.
How it worksFive layers: (1) Proximity — any files/directories overlap with the current change manifest → +30; (2) Recurrence — recurrence.count >= 2 → +20; (3) Severity — incident=40, block=25, warn=10, info=5 added to score; (4) Validity — expired == false required else drop, plus 4b Superseded — an entry whose id appears in a newer entry's supersedes is dropped; (5) Phase — must match the current phase or be phase=any. A separate --scope flag (team|personal|all, default all) filters entries by their stored scope.
Why it worksLoading every lesson eagerly wastes the context budget and buries the relevant few; deterministic scoring by proximity and severity plus hard drops for stale/superseded/out-of-phase entries means the model sees the handful of lessons that actually apply to the work in front of it, not a wall of history.
#scripts/validate-toolkit.sh
script
The structural gate — version sync, skill anatomy, frontmatter, budgets, and wiring, checked before every commit.
Trigger Bash script run before every commit: bash scripts/validate-toolkit.sh; also --task-scoped <comma-separated file list> for a fast wiring check
What it doesThe canonical structural gate (rule S3.9). It verifies the toolkit is internally consistent: required files present, the three version numbers in sync, every skill follows its anatomy and line budget, references carry frontmatter, generated indexes are fresh, and the reviewer agents exist. Rule C0.8 requires seeing its "Toolkit validation passed." line before reporting any change complete.
ExampleBefore committing a new skill, the engineer runs the gate:
$ bash scripts/validate-toolkit.sh
Skill descriptions: 5820 chars (~1455 tokens) / 7000-char budget.
Toolkit validation passed.
Had the skill's frontmatter name not matched its directory, or manifest/plugin/marketplace versions differed, or CLAUDE.md exceeded 120 lines, it would instead print ERROR: ... and exit 1 on the first violation.
How it worksPure bash on the coreutils/grep/sed/awk/find/git + python3 baseline (S3.3). A full run checks: required files (README, CONTRIBUTING, AGENTS, manifest, plugin.json, the generic references, and the three reviewer agents compliance-reviewer/test-reviewer/architecture-reviewer), settings.json carries permissions.deny and hooks blocks, every hooks/ and scripts/ shell script is chmod +x with set -euo pipefail, the tier-2 queue files exist (hooks/lib/skill-queue.sh, hooks/userprompt-dispatch.sh), triggers.index is in sync, the toolset registry resolves, the version triple matches (manifest==plugin==marketplace), every manifest source path exists, skill anatomy by type (entry-point/tech-stack/phase-based/workflow), line budgets (CLAUDE.md 120-line hard cap, skills 500/1000, rules 120), the skill-description budget (200-char cap per skill, 7000-char aggregate), reference frontmatter (description/globs/alwaysApply), references.index and rule-index freshness, a skill bash lint, and read-only enforcement of mcp/src/tools. A --task-scoped <csv> mode instead verifies only that touched skills/hooks/agents/references are fully wired (name matches dir, registered in manifest/plugin/settings/index). It fails fast with ERROR: or prints "Toolkit validation passed.".
Why it worksA plugin distributed to many repos breaks silently when a skill is not registered, versions drift across the three manifests, or a generated index goes stale — failures that surface downstream as missing routes or a bad install. Making one deterministic script the gate, and requiring its green line before "done," converts those into a fast local failure instead of a shipped bug.
#scripts/repo-health-score.sh
script
The 12-asset scorecard engine — grades a repo pass/partial/fail across 3 buckets and awards a medal.
Trigger Bash script: bash scripts/repo-health-score.sh (markdown) or --json; also invoked by the repo-health skill
What it doesThe deterministic engine behind repo-health. It checks 12 named assets across three buckets — AI Context, Dev Workflow, Onboarding — returning pass/partial/fail/na for each. It computes a medal from the pass count and renders either a markdown scorecard or JSON.
Examplebash scripts/repo-health-score.sh prints a header like Medal: 🥈 silver — 6 pass / 3 partial / 3 fail (of 12 scoring assets; 0 n/a) followed by each asset with a 🟩/🟨/⬜ icon, grouped by bucket:
### AI Context
- 🟩 **1. CLAUDE.md present** — 92 lines
- 🟨 **2. Architecture principles tagged** — only 3 tagged principles
- ⬜ **4. Lessons captured** — tasks/lessons.md missing
bash scripts/repo-health-score.sh --json | jq .medal returns the medal string for dashboards.
How it worksEach asset records name|bucket|status|note. AI Context (4): CLAUDE.md non-empty; architecture-principles.md with ≥5 lines tagged [EXTRACTED|INFERRED:N.N|AMBIGUOUS|MINED:feedback]; .claude/tech-stack matching a tech-stack-<stack> skill; tasks/lessons.md with ≥1 '## ' entry. Dev Workflow (4): docs/specs and docs/plans each with ≥1 *.md modified in the last 90 days; manifest.json==plugin.json version (via python3); validate-toolkit.sh exits 0. Onboarding (4): README non-empty; build/test commands grep-found in CLAUDE.md/README/tech-stack skill; .claude/rules with ≥2 files; .gitignore excludes settings.local.json (plus analytics.json if present). Medal from pass count: 🏆 platinum ≥10, 🥇 gold ≥8, 🥈 silver ≥6, 🥉 bronze ≥4, else (no medal); the denominator is 12−na. JSON is rendered via an embedded python3 heredoc.
Why it works"Is this repo ready for AI?" is otherwise a vibe. Encoding it as 12 deterministic, individually-cited checks with a bounded medal makes the answer reproducible, diffable over time, and safe to run in CI — and keeps the standard honest: partial never counts toward the medal, and na assets drop out of the denominator.
#.claude/references/repo-health-assets.md
reference
Canonical 12-asset checklist, pass/partial/fail rubric, and medal thresholds behind the repo-health scorecard.
Trigger Loaded on-demand (frontmatter globs it to scripts/repo-health-score.sh and .claude/skills/repo-health/**); read by engineers as the canonical rubric
What it doesThe source-of-truth reference defining what "AI-ready" means for a repo. It lists the 12 assets and their buckets, the exact pass/partial/fail/na rubric for each, the medal thresholds, and when to re-run. The scorecard script and skill must not add assets without updating this file.
ExampleA reviewer wants to know why asset #2 is only "partial." They open repo-health-assets.md and read the rubric: architecture-principles.md needs ≥5 tagged principles for pass, 1–4 for partial, and missing-or-untagged for fail — with the tag pattern [EXTRACTED|INFERRED:N.N|AMBIGUOUS|MINED:feedback]. The medal table confirms ≥8 pass = 🥇 gold.
How it worksFrontmatter (alwaysApply:false) globs it to scripts/repo-health-score.sh and .claude/skills/repo-health/**, so it loads only when relevant. It contains the bucket table (AI Context / Dev Workflow / Onboarding), a per-asset rubric with concrete thresholds, the medal-thresholds table (partial excluded from the medal, na excluded from the 12−na denominator), and 'when to re-run' guidance. The pattern is borrowed from github.com/johnpapa/ai-ready (bounded scorecard + medal). The repo-health skill's Red Flags treat the 12 assets as canonical — extending them requires editing this reference.
Why it worksIf the checklist lived only in the script, the definition of "AI-ready" would drift each time someone tweaked a grep. Pinning the 12 assets, thresholds, and medal cutoffs in one human-readable reference keeps the standard stable, auditable, and shared across the script, the skill, and the engineers reading their score.
#hooks/security-gate.sh
hook
PreToolUse Bash gate that blocks nuanced destructive commands the settings.json deny-list misses (exit 2 = blocked).
Trigger Automatic PreToolUse hook on the Bash tool (wired in .claude/settings.json with matcher "Bash", timeout 5). Receives the tool payload as JSON on stdin.
What it doesBefore any Bash command runs, this gate inspects the command string and hard-blocks a small set of catastrophic operations: destructive database statements, force-pushes to protected branches, and recursive force-deletes on broad paths. It is the safety net that catches destructive intent expressed in ways the exact-match deny-list in settings.json cannot. Exit 0 allows the command; exit 2 blocks it and the reason is printed to the agent.
ExampleThe agent tries to run git push --force origin main. The gate's regex git\s+push\s+.*--(force|force-with-lease)\s+.*\b(main|master)\b matches, the hook exits 2, and the agent sees:
BLOCKED: Force push to main/master is not allowed.
Similarly psql -c "DROP TABLE users" returns BLOCKED: Destructive database operation detected. Use a migration instead.
How it worksSources hooks/lib/hook-io.sh, then extracts tool_name and the command via mtk_extract_tool_name / mtk_extract_command. Non-Bash payloads exit 0; a Bash payload it cannot parse fails closed (exit 2). It runs three grep -qiE / grep -qE checks: (1) DROP TABLE|DATABASE|SCHEMA, TRUNCATE TABLE, or DELETE FROM ...; (2) git push --force / --force-with-lease targeting main|master; (3) rm -rf on ., /, ~, or $HOME. Any match prints a BLOCKED message to stderr and exits 2. Everything else exits 0. The header explicitly notes the settings.json deny list handles exact-pattern matches; this script catches the alternate-syntax and disguised variants.
Why it worksDeny-lists match literal strings, so an agent (or a paraphrased command) can slip a destructive operation past them via flags, pipes, or alternate syntax like --force-with-lease. This deterministic gate stops the highest-blast-radius mistakes — dropped tables, rewritten protected-branch history, wiped working trees — before they execute, with no reliance on the model's judgment.
#hooks/read-guard.sh
hook
PreToolUse read gate that blocks pulling secret-bearing files into context (exit 2) and warns on generated/vendored noise dirs.
Trigger Automatic PreToolUse hook on Read|Grep|Glob (wired in .claude/settings.json, timeout 5). Env knob: MTK_READ_GUARD=advisory downgrades blocks to warnings.
What it doesBefore the agent reads, greps, or globs a file, this gate classifies the target. Secret-bearing files (.env, private keys, credential files) are blocked so credentials never enter the model's context without a human in the loop. Generated or vendored directories (bin, obj, node_modules, dist, .venv) get an advisory warning but are allowed. Everything else passes silently.
ExampleThe agent tries to Read deploy.pem. The basename matches the *.pem secret class — a private-key vector the settings.json deny-list does NOT cover — the hook exits 2, and prints:
READ-GUARD: blocked read of secret-bearing file 'deploy.pem'. Reading credentials into context requires explicit human approval — do NOT attempt to work around this. STOP and ask the engineer...
Reading node_modules/react/index.js instead only warns: READ-GUARD (advisory): 'node_modules/react/index.js' is under a generated/vendored path, then allows it. (This guard fired for real during fact-checking when a Read hit core/secrets.txt, whose basename matches the secrets.* class.)
How it worksSources lib/hook-io.sh and lib/mtkignore.sh. Classifies by basename: allowlists *.example|*.template|*.sample|*.dist; treats .env/.env.* (note: only a leading-dot .env or .env.<suffix> — a name like prod.env does NOT match), *.pem/*.key/*.pfx/*.p12/*.kdbx/*.keystore/*.jks, id_rsa*/id_dsa*/id_ecdsa*/id_ed25519*, secrets.*/*.secret/*.secrets, and .npmrc/.netrc/.pgpass/credentials as secrets. On a secret: if MTK_READ_GUARD=advisory it warns and exits 0; otherwise it checks a per-day approval list at $TMPDIR/mtk-readguard-approved-<repo-cksum>-<date> and exits 2 if not listed. The block message deliberately omits any self-approval recipe — the human grants access out-of-band. For noise dirs it loads ignore patterns via mtk_load_ignore_patterns, normalizes to a repo-relative path, and emits an advisory (exit 0).
Why it worksAn agent that can silently read .env or a private key can leak credentials into its context window and downstream transcripts. Making the secret read a hard block with no agent-runnable bypass keeps a human in the loop for credential access. The noise-dir advisory separately stops the agent from wasting context grounding itself on machine-generated output instead of source.
#hooks/scope-guard.sh
hook
Advisory PreToolUse Edit|Write hook that flags edits to files not declared in the active spec's manifest (never blocks).
Trigger Automatic PreToolUse hook on Edit|Write (wired in .claude/settings.json, timeout 5). No-op when there is no active spec JSON sidecar.
What it doesWhen the agent edits or writes a file, this hook checks whether that file is listed in the active spec's change_manifest or test_manifest. If the file is undeclared, it prints a scope-creep warning naming the spec. It is advisory only (exit 0) — it never blocks the edit, it surfaces drift so the engineer can update the spec or stop the agent.
ExampleDuring an approved spec whose manifest lists only src/PaymentService.cs, the agent edits src/EmailSender.cs. The hook prints:
SCOPE GUARD: src/EmailSender.cs is not in the approved spec (2026-07-09-refund-flow). If this change is necessary, update the spec's change_manifest first. Undeclared file modifications are the #1 source of spec drift.
How it worksSources lib/hook-io.sh, resolves the repo-relative path, then finds the active spec JSON sidecar under docs/specs (most recently modified within 7 days via find -mtime -7). It extracts every "path" entry inside the change_manifest and test_manifest sections (awk with a grep/sed fallback for older awk), always appends docs/specs/, docs/plans/, tasks/, and .claude/ as allowed, and caches the allow-list keyed by spec path + mtime. If the edited path matches no allowed prefix it calls mtk_record_scope_guard_warning and prints the SCOPE GUARD line. No active spec sidecar means a silent no-op.
Why it worksUndeclared file modifications are the number-one source of spec drift — the implementation quietly grows beyond what was approved. By echoing the manifest boundary at the moment of an out-of-scope edit, the guard keeps the actual change honest to the approved plan without ever getting in the way of legitimate work.
#hooks/workflow-continuation.sh
hook
Stop-hook nudge that surfaces one line when a session ends with an active workflow that still has unfinished batches.
Trigger Automatic Stop hook (wired in .claude/settings.json). Tier-2 (skill-invoking); respects MTK_HOOKS_TIER2 (default 1, set 0 to silence).
What it doesWhen a session stops, this hook checks whether the newest workflow is still active with batches left to do. If so, it prints a single advisory line telling the engineer their options: resume the next batch, hand off, or abandon the workflow. It never blocks and never auto-continues work — the human decides.
ExampleA session ends mid-implementation. The hook reads the newest .mtk/workflows/*.json, sees status active with 4 batches total and 2 done, and prints:
WORKFLOW IN PROGRESS: 2026-07-09-refund-flow has 2 of 4 batches unfinished (2 done). Resume it (continue the next batch per tasks/todo.md), hand off (`handoff` skill), or close it: bash scripts/workflow-artifact.sh abandon 2026-07-09-refund-flow --reason "<why>".
How it worksExits early if MTK_HOOKS_TIER2=0. Sources lib/hook-io.sh, finds the newest .mtk/workflows/*.json by portable stat mtime, and extracts fields with jq-free grep/sed helpers (json_num/json_str per S3.3). It proceeds only if status is active and batches_completed < batches_total. It debounces on a marker at $TMPDIR/mtk-wf-continuation-<uuid-cksum> keyed on the done count, so it fires at most once per (workflow, batches_completed) — no spam within a Stop burst, but it re-notifies after a batch actually completes.
Why it worksLong multi-batch implementations routinely get interrupted by context limits, crashes, or the engineer walking away. Without a nudge, an in-progress workflow silently strands — the next session has no idea work was mid-flight. This surfaces the exact state (X of N batches) and the three concrete exits, so nothing is lost and the engineer, not the agent, chooses what happens next.
#hooks/verify-completion
hook
Stop hook that flags a strong completion claim ('task complete', 'all done') when it lacks fresh cited verification evidence.
Trigger Automatic Stop hook (wired in .claude/settings.json and, for plugin installs, hooks/hooks.json; no arguments — reads the Stop payload from stdin). On a gap it blocks the stop once with a reason; never when stop_hook_active is set.
What it doesWhen the agent's final message makes a strong completion claim, this hook checks the session state for evidence that verification actually ran after the last edit. If there's no fresh verification, if an edit landed after the last verification (criteria re-armed), or if the message doesn't cite command output, it blocks the stop once with a one-line VERIFICATION GAP reason telling the agent to run the tech-stack verification command and cite exit code plus pass/fail counts.
ExampleThe agent ends with 'Implementation complete, ready for review.' but never ran the test suite this session. The hook matches the strong-claim regex, finds last_verification_epoch = 0, and blocks the stop with:
VERIFICATION GAP: completion claim without a fresh verification command. Run the verification command from your active tech stack (.claude/skills/tech-stack-{stack}/SKILL.md → ## Build & Test Commands) and cite exit code + pass/fail counts. For toolkit work: 'bash scripts/validate-toolkit.sh' and cite 'Toolkit validation passed.'
How it worksSources lib/hook-io.sh, reads the Stop payload from stdin and derives the claim from the transcript's last assistant message (transcript_path), and matches a deliberately narrow STRONG_CLAIM regex (all done|task complete|work complete|implementation complete|ready for review|fully verified|✅ done|🟢 done, plus top-line 'done/completed/fixed/verified/finished' with terminal punctuation) — tuned to skip casual mentions. On a match it loads session state via mtk_session_file / mtk_load_session_state and checks three conditions in order: no verification this session (last_verification_epoch=0); an edit after the last verification (last_edit_seq > last_verification_seq → 'criteria re-armed'); or a message missing execution evidence (an EVIDENCE regex covering exit codes, test-runner output, and 'Toolkit validation passed'). Each prints its own VERIFICATION GAP line. It always exits 0 (advisory).
Why it worksThe classic AI failure mode is declaring victory without running anything — 'done' with no test output behind it. This hook makes the completion claim itself the trigger for demanding fresh, cited evidence, and the 're-armed' check specifically catches the case where the agent verified, then edited again, then claimed done on stale results. It blocks at most once per stop — never when stop_hook_active is set — so it can't wedge a session.
#hooks/pre-commit-linters.sh
script
Deterministic lint engine that scans added diff lines against active guard packs and emits JSON findings (confidence 100).
Trigger Bash script: hooks/pre-commit-linters.sh [--cached | --head] [--stack <name>] [--human]. Run over staged changes as the deterministic first pass before AI review.
What it doesThis is the engine that runs MTK's guard packs against a diff. It scans only added lines in staged (or HEAD) changes, matches each against the active packs' regex rules, and emits findings in the review-finding schema with source 'linter' and confidence 100. It computes a verdict — PASS, or NEEDS_CHANGES if any critical rule matched — and outputs JSON (default) or a human-readable summary. It feeds the AI review pass rather than replacing it.
Example$ hooks/pre-commit-linters.sh --human
Linter verdict: NEEDS_CHANGES (critical=1 warning=0 suggestion=0)
{"id":"L001","severity":"critical","confidence":100,"source":"linter","rule":"RAW-SQL-INTERPOLATED","file":"src/Repo.cs","line":42,...}
A staged line using FromSqlRaw($"...{id}") trips the .NET pack's critical SQL-injection rule and the commit is flagged NEEDS_CHANGES.
How it worksDetects the stack from .claude/tech-stack (or --stack), then builds the pattern list in discovery order: core/ (always) → stack-<name>/ → domain-<name>/ (from .claude/domains, one per line) → project/ (with flat-file fallbacks for pre-6.2 layouts). It runs git diff --cached -U0 --no-color (or git diff HEAD -U0 --no-color with --head), an awk pass extracts file/line/content for added lines only, and each pack rule (tab-separated RULE_ID|SEVERITY|regex|rationale|fix) is matched with grep -qEi (case-insensitive ERE). Matches become JSON findings; the verdict is NEEDS_CHANGES when critical>0.
Why it worksSome AI failure modes are mechanical and falsifiable — a hardcoded secret, interpolated SQL, a float used for money — and don't need a reasoning model to catch. Running them deterministically over the diff gives a cheap, 100%-confidence floor that never rationalizes or misses on a bad day, freeing the reviewer agents to spend judgment on the things regex can't decide.
#hooks/linter-patterns/ (guard packs)
reference
Composable, shippable regex rule packs — core, per-stack, per-domain, and project-local — that are MTK's deterministic review units.
Trigger Loaded on-demand by hooks/pre-commit-linters.sh; packs auto-activate by directory (core always; stack-/domain- by .claude/tech-stack and .claude/domains). Authoring guide in .claude/references/guard-packs.md.
What it doesA guard pack is a tab-separated file of regex rules (RULE_ID, SEVERITY, ERE_REGEX, RATIONALE, SUGGESTED_FIX) that the linter engine runs against staged changes. Packs are organized so quality gates ship as reusable units instead of prose checklists: core packs always apply, stack and domain packs turn on to match the repo, and a gitignored project/ pack holds local overrides. Only 'critical' severity blocks a commit; 'warning' and 'suggestion' are advisory.
ExampleThe finance domain pack (domain-finance/patterns.txt) ships FLOAT-MONEY-CS (critical) — (float|double)[[:space:]]+(amount|balance|price|fee|cost|total|sum|payment|rate) — and FIN-AUDITED-DELETE (warning), which flags Transactions.Remove(...) as breaking the audit trail. Core packs ship SECRET-HARDCODED and AWS-ACCESS-KEY (secrets.txt) plus SLOP-SKIP-TEST (slopwatch.txt — a disabled test that may be masking a failure); the .NET pack ships RAW-SQL-INTERPOLATED and WEAK-HASH (MD5/SHA1).
How it worksDirectory layout under hooks/linter-patterns/: core/ (docdrift.txt, secrets.txt, slopwatch.txt — always active), stack-<name>/ (stack-dotnet, stack-python, stack-typescript — active when .claude/tech-stack matches), domain-<name>/ (domain-finance — active when .claude/domains lists it), and project/ (gitignored, .gitkeep placeholder). pre-commit-linters.sh concatenates active packs in that order. Rules use POSIX ERE matched case-insensitively via grep -Ei, so PCRE features like lookahead don't work. Authoring: keep rules conservative, reserve 'critical' for almost-always-wrong smells, add a test under tests/hooks/, and run scripts/validate-toolkit.sh. Packs are the deterministic floor; reviewer agents (compliance-reviewer, silent-failure-hunter) are the judgment ceiling.
Why it worksProse checklists rot and depend on someone remembering to apply them. Packaging quality gates as composable, testable, auto-activating regex units means a repo automatically gets exactly the checks its stack and domain warrant — finance repos catch float money and audited deletes, .NET repos catch interpolated SQL — with a stable RULE_ID for every finding and a conservative-severity discipline that keeps the packs from crying wolf.
#hooks/format-on-edit.sh
hook
PostToolUse hook that auto-formats the file the agent just edited, dispatching by extension; never blocks the edit.
Trigger PostToolUse hook on Edit|Write. Command string bash $CLAUDE_PLUGIN_ROOT/hooks/format-on-edit.sh (matcher Edit|Write), added to a target repo's .claude/settings.json during setup via the tech-stack skill's 'Settings Additions' block. NOTE: it is not present in the toolkit's own .claude/settings.json — that file wires only the guard hooks. Reads the edited file_path from stdin JSON.
What it doesAfter the agent edits or writes a file, this hook runs the appropriate formatter for that file's language so the committed diff stays clean and consistent. Formatter selection is by file extension. Any formatter failure is reported to stderr (so it shows in the hook log) but never blocks the edit.
ExampleThe agent writes src/handlers/refund.py. The hook sees the .py extension and, if ruff is installed, runs ruff format then ruff check --fix on that file; the saved file comes back formatted with no separate 'please run the formatter' step. If ruff is absent it falls back to black --quiet; if neither is installed, the hook silently does nothing for that language.
How it worksSources lib/hook-io.sh, extracts file_path via mtk_extract_file_path, and dispatches on the extension: ts/tsx/js/jsx/mjs/cjs → npx --no-install biome format --write then prettier as fallback; py → ruff format + ruff check --fix, else black --quiet; cs → dotnet format --include. Each formatter is probed with command -v / npx --no-install so a missing tool is a no-op. run_formatter captures the exit code and logs 'mtk format-on-edit: <label> failed ...' on failure. The hook always exits 0.
Why it worksFormatting drift produces noisy diffs and review friction, and an agent that hand-formats wastes tokens and gets it inconsistently. Doing it deterministically the moment a file is written keeps every edit canonical without adding a manual step, and failing soft means a missing or broken formatter never wedges the agent's edit.
#hooks/check-prerequisites.sh
script
Reports which recommended dev tools are missing (shellcheck, jq, stack-specific) as warnings — never blocks.
Trigger Bash script: bash hooks/check-prerequisites.sh [stack] (stack: dotnet|python|typescript, else reads .claude/tech-stack). Called by setup-bootstrap after tech-stack detection.
What it doesDuring setup, this script probes for the development tools that make MTK's hooks and linters work best and reports any that are missing. It checks core tools for every stack plus stack-specific tools, and prints install hints for anything absent. It only warns — the tools are optional and MTK works without them.
Example$ bash hooks/check-prerequisites.sh python
Prerequisites: 2 recommended tool(s) not found:
⚠ shfmt — formats shell scripts consistently (brew install shfmt)
⚠ mypy — static type checking (pip install mypy)
These are optional but improve hook and linter quality.
Install them when convenient — MTK works without them.
How it worksResolves the stack from the argument or .claude/tech-stack. check_tool uses command -v to probe each tool and, on a miss, records a ⚠ <name> — <purpose> (<install hint>) line. Core tools (all stacks): shellcheck, shfmt, jq. Stack-specific: dotnet → dotnet, dotnet-format; python → python3, ruff, mypy, pytest; typescript → node plus the active package manager from .claude/tech-stack-pm. It prints either 'Prerequisites: all recommended tools found.' or, when any are missing, a header with the count followed by the ⚠-bulleted list (the individual lines are bulleted, not numbered). It never exits non-zero.
Why it worksMTK's deterministic layer leans on external tools (shellcheck for hook quality, jq for JSON, ruff/dotnet-format for formatting). Silently degrading when they're absent would leave engineers wondering why a linter or formatter did nothing. Surfacing the gaps with exact install hints at setup time — as warnings, not gates — lets the engineer close them on their own schedule without ever blocking onboarding.
#hooks/session-start
hook
Injects MTK's active banner plus session-recovery, version-drift, and personal-lesson notices at session start.
Trigger Automatic SessionStart hook (matcher "startup|clear|compact"), wired in hooks/hooks.json
What it doesRuns at the start of every new/cleared/compacted session. It announces that the toolkit is active and names the entry points, surfaces any in-progress spec/plan/todo so the agent resumes instead of restarting, warns when the installed MTK version has drifted from the source it was installed from, lists personal lessons in play, and silently rebuilds the optional MCP server if its bundle is stale. It also adapts its output format to the host CLI.
ExampleOpen Claude Code in a repo where you wrote a spec yesterday. The hook emits a single JSON context line:
{"context": "Moberg toolkit is active. Available entry points: /mtk and /mtk-setup. Skills load on demand via progressive disclosure. SESSION RECOVERY: Found recent spec at docs/specs/2026-07-08-payments.md. You may have an in-progress feature. Read the spec to resume where you left off."}
How it worksPlatform detection reads CLAUDE_PLUGIN_ROOT / CURSOR_PLUGIN_ROOT / COPILOT_CLI / GEMINI_EXTENSION_ROOT and emits either {"context":"..."} or the SDK-standard {"role":"system","content":"..."}. Recovery scans docs/specs for *.md modified in the last day, then docs/plans, then tasks/todo.md for '[ ]' items (first match wins). Version drift compares .claude/mtk-version.json against ${CLAUDE_PLUGIN_ROOT}/.claude/manifest.json and prints 'MTK update available: X → Y'. Personal lessons come from counting '## ' headers in .claude/lessons/personal.md. If an mcp/ directory exists and node is on PATH, and dist/mtk-mcp-server.cjs is missing or any file under mcp/src is newer, it runs scripts/build-mcp.sh --quiet (non-blocking). JSON escaping is done with bash parameter substitution only.
Why it worksA fresh session has no memory of in-flight work, so without recovery the agent silently re-starts or duplicates a half-finished feature. The advisory version check keeps installs from quietly running an outdated toolkit, and the MCP self-build keeps the optional deterministic layer current without any manual npm step.
#hooks/context-budget.sh
hook
Counts reads/edits/ops per session and nudges a checkpoint or handoff before the context window degrades.
Trigger Automatic PostToolUse hook (fires after every tool), wired in .claude/settings.json; timeout 3s
What it doesAfter every tool call it updates per-session counters and, when work crosses a threshold, prints a one-line advisory. It tracks unique files read, modifications, total operations, and an estimate of context tokens consumed, so it can tell you to narrow focus, commit a checkpoint, or reset before quality drops. It never blocks — it only nudges, once per threshold.
ExampleAfter the 30th distinct file read in a session, the hook prints to the transcript:
CONTEXT BUDGET: 30 unique files read this session. Consider narrowing focus to the files that matter for your current task. Use context-engineering to load only relevant references.
At ~60% of the window it instead suggests capturing state with the handoff skill and starting fresh.
How it worksState lives in a per-project/per-day temp file (mtk_session_file). Each Read accumulates bytes_read for new unique files (capped at 100k each); Edit/Write bump mods; Bash that matches mtk_command_is_verification records last_verification_*. Thresholds: 30 unique files, 40 modifications, 120 operations, and a read-bytes floor estimate (bytes_read/4) reaching MTK_CONTEXT_BUDGET_PCT (default 60) of MTK_CONTEXT_WINDOW_TOKENS (default 200000). Each threshold fires once via warned_files/warned_mods/warned_ops/warned_ctxpct flags flipped with sed. Concurrency-safe via an mkdir advisory lock around the load-modify-save cycle (hooks/lib/hook-io.sh).
Why it worksContext degrades silently as the window fills, and the agent has no reliable internal sense of how full it is. Deterministic byte/operation counting gives an objective, cheap signal to checkpoint or hand off before answers get worse — the 'reset before degradation' idea, made measurable.
#hooks/session-analytics.sh
hook
Persists per-session stats into .claude/analytics.json so teams can track MTK adoption and effectiveness.
Trigger Automatic Stop hook (fires when the agent stops), wired in .claude/settings.json
What it doesWhen a session ends it rolls that session's counters into a cumulative, gitignored analytics file. It accumulates sessions, operations, modifications, specs created, lessons captured, scope-guard warnings, benchmark runs, queue activity, and an estimated-context-tokens figure. Trivial sessions (fewer than 5 operations) are skipped so noise doesn't inflate the numbers.
ExampleAfter a real working session, .claude/analytics.json grows:
{
"sessions": 43,
"total_operations": 1291,
"total_modifications": 575,
"specs_created": 5,
"lessons_captured": 12,
"bytes_read": 4120000,
"estimated_context_tokens": 1030000
}
The toolkit-health skill later reads this file to report adoption.
How it worksReads the session state file written by context-budget.sh (via mtk_load_session_state). Exits early if session_ops < 5. Initializes .claude/analytics.json if absent, then reads current values with jq-free grep/sed helpers, increments them, computes estimated_context_tokens = bytes_read/4, counts new specs in docs/specs (*.json newer than the analytics file) and lessons via '## ' headers in tasks/lessons.md, and writes atomically (temp file + mv) to avoid truncation races. Queue counters (queue_writes/queue_drains/queue_expired) are carried through from the tier-2 queue.
Why it worksAdoption is organic on this team (no mandates), so a lightweight, gitignored, no-dependency ledger is the only honest way to see whether MTK is actually being used as intended — and it gives toolkit-health real numbers instead of guesses.
#hooks/post-compact.sh
hook
Re-injects on-disk state (stack, specs, plans, tasks, handoffs, active workflow) after auto-compaction.
Trigger Automatic SessionStart hook with matcher "compact" (fires after auto-compaction), wired in .claude/settings.json and hooks/hooks.json; timeout 5s
What it doesAuto-compaction throws away conversation history, so this hook reconstructs the essentials from disk and feeds them back as a single recovery context. It reminds the agent of the active tech stack, any recent spec/plan, incomplete tasks, the latest handoff, and the active durable workflow — so the post-compaction session keeps working instead of losing the thread.
ExampleMid-implementation the session auto-compacts. Immediately after, the hook emits:
{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "POST-COMPACTION RECOVERY: Active tech stack: dotnet. Package manager: nuget. Active spec: docs/specs/2026-07-08-payments.json — read before resuming implementation or review. Incomplete tasks in tasks/todo.md — check before starting new work. Active workflow: 2026-07-08-payments (type=implement, phase=implementation, last_event=batch_verified) — read .mtk/workflows/... before advancing; do not re-init."}}
How it worksReads .claude/tech-stack (+ .claude/tech-stack-pm), docs/specs/*.json and docs/plans/*.md modified in the last day, '[ ]' items in tasks/todo.md, docs/handoffs/*.md, and the active workflow artifact under .mtk/workflows/ (grep for status active, then workflow_type and phase_cursor, plus the last event_type from the paired .events.jsonl). It concatenates these into one string, escapes it with bash-only substitution, and prints a SessionStart hookSpecificOutput.additionalContext envelope carrying "POST-COMPACTION RECOVERY: ...". Per rule S3.11 it does no blocking and has no side effects.
Why it worksCompaction is the single biggest silent context-loss event: the agent can forget it was mid-feature and start fresh or contradict prior decisions. Re-hydrating from durable on-disk artifacts (rather than chat memory) is what lets orchestration survive compaction at all.
#hooks/pre-compact-snapshot.sh
hook
Stashes uncommitted work right before auto-compaction so context loss never costs in-flight changes.
Trigger Automatic PreCompact hook (fires before auto-compaction), wired in .claude/settings.json and hooks/hooks.json; opt-in for plugin installs via MTK_COMPACT_SNAPSHOT=1; timeout 8s
What it doesJust before an auto-compaction, this hook takes a safety snapshot of the working tree using git stash and immediately re-applies it, so the tree looks unchanged but a recoverable checkpoint exists. It only acts on automatic compaction, never touches a clean tree, and stays out of the way during rebases/merges. It logs every action for auditability.
ExampleYou have uncommitted edits when an auto-compaction triggers. The hook prints to stderr and appends to a log:
[mtk] precompact snapshot saved → mtk-precompact-2026-07-09T14:03:11Z (branch=feat/payments)
If anything went wrong you recover with git stash apply (or scripts/mtk-recover.sh).
How it worksReads the hook JSON payload; it exits only when the payload declares a non-auto trigger (manual /compact is respected). When the trigger is "auto" or the payload has no trigger field, it proceeds. Requires a git work tree; skips when a rebase-merge/rebase-apply/MERGE_HEAD/CHERRY_PICK_HEAD/REVERT_HEAD marker exists; skips clean trees (no staged/unstaged/untracked changes). Otherwise runs git stash push --include-untracked -m mtk-precompact-<UTC-timestamp> then git stash apply --index (falling back to plain apply) so the tree — including the staged/unstaged split — is left unchanged. Every outcome (saved/skipped/failed) is appended to .claude/observability/precompact-snapshots.log. A trap 'exit 0' ERR guarantees it never blocks compaction.
Why it worksAuto-compaction can be followed by the agent doing something destructive or losing awareness of edits; a pre-compaction stash means in-flight work is always recoverable even in the worst case. Skipping manual compaction and in-progress git operations keeps the safety net from interfering with deliberate user actions.
#hooks/compress-monitor.sh
hook
Flags large Bash outputs that landed in context without being piped through mtk-compress.sh.
Trigger Automatic PostToolUse hook (matcher Bash), wired in .claude/settings.json; timeout 3s
What it doesAfter a Bash tool call, if the command dumped a large amount of raw output straight into context and did not already compress it, this hook suggests re-running it through the compression helper. It is advisory — it does not alter the result — and it picks a sensible compression mode based on what the command looked like.
ExampleYou run dotnet test and it prints 12,000 chars of output. The hook returns:
{"continue": true, "additionalContext": "💡 mtk-compress: Bash output was 12,000 chars (warn ≥ 5,000). Pipe long output through `bash scripts/mtk-compress.sh tests` to reclaim ~70-95% of context tokens. Set MTK_COMPRESS_MONITOR_DISABLED=1 in .claude/settings.local.json env to silence."}
How it worksA small python3 program parses the PostToolUse payload: it exits unless tool is Bash, the command does not already contain 'mtk-compress' (or 'MTK_COMPRESS_DISABLED'), and the result length is >= MTK_COMPRESS_WARN_CHARS (default 5000). It infers a mode from the command shape — 'tests' for test runners, 'logs' for builds/tsc/biome, 'html' for curl of HTML, 'json' for jq/cat of .json, else 'auto' — and emits {"continue": true, "additionalContext": "..."}. The whole hook is disabled per-machine with MTK_COMPRESS_MONITOR_DISABLED=1.
Why it worksVerbose command output is one of the fastest ways to burn the context window on low-value text; nudging the engineer to pipe it through the compressor reclaims most of those tokens without losing the signal the agent actually needs.
#hooks/userprompt-dispatch.sh
hook
Single UserPromptSubmit entry point that merges tier-1 nudges, trigger hints, and the tier-2 queue drain.
Trigger Automatic UserPromptSubmit hook (runs on every user prompt), wired in .claude/settings.json; timeout 5s
What it doesOn every user prompt this one hook does three jobs and merges them into a single injected-context payload: it emits inline tier-1 nudges (e.g. correction detection), surfaces keyword-triggered skill hints, and drains the tier-2 skill queue that other hooks wrote since the last prompt. Consolidating everything into one script avoids the unguaranteed ordering of multiple UserPromptSubmit hooks.
ExampleYou type a new prompt after a hook queued a follow-up skill. The dispatcher returns:
{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"MTK-QUEUED (1 pending):\n- planning-and-task-breakdown — spec approved. Consider: invoke the planning-and-task-breakdown skill now."}}
How it worksSources hooks/lib/skill-queue.sh (and, if present, lib/prompt-nudges.sh and lib/trigger-hints.sh). Short-circuits via mtk_queue_enabled (MTK_HOOKS_TIER2). Extracts the prompt with the escape-aware mtk_extract_json_string. Collects tier-1 nudges (mtk_collect_prompt_nudges) and trigger hints (mtk_collect_trigger_hints, S2.22). Drains .claude/queue/*.json newest-first: deletes entries older than their ttl_hours, dedups by skill|source_hook, caps at 3 surfaced per turn, and records queue_drains/queue_expired counters. Output is grouped under MTK-NUDGE / MTK-TRIGGERS / MTK-QUEUED and returned as escaped additionalContext.
Why it worksSome suggestions can't be delivered at the moment they arise — they must wait for the next prompt or survive a session boundary. A single deterministic dispatcher gives one predictable place for all prompt-time context injection, so nudges never race each other or get silently dropped by hook ordering.
#.claude/queue/ (tier-2 skill queue)
concept
Deferred, advisory skill-invocation queue: hooks drop JSON entries that the prompt dispatcher surfaces next turn.
Trigger Named mechanism: writers call queue_skill() in hooks/lib/skill-queue.sh; drained by hooks/userprompt-dispatch.sh; kill-switch env MTK_HOOKS_TIER2
What it doesThis is the mechanism behind 'tier-2' hooks. When a hook wants to suggest a skill but the moment isn't a user prompt — or the suggestion should persist across a session boundary — it writes a small JSON entry under .claude/queue/ instead of nudging inline. The next user prompt drains it. Entries are advisory ('propose, never act'): the agent decides whether to invoke.
ExampleA hook fires: queue_skill planning-and-task-breakdown "spec approved" spec-approval-trigger.sh. That writes .claude/queue/1751...-planning-and-task-breakdown-<hash>.json:
{ "queued_at": "2026-07-09T14:03:11Z", "queued_epoch": 1751000000, "skill": "planning-and-task-breakdown", "reason": "spec approved", "context": { "excerpt": "..." }, "ttl_hours": 24, "source_hook": "spec-approval-trigger.sh" }
On your next prompt the dispatcher surfaces it as an MTK-QUEUED line.
How it worksqueue_skill <skill> <reason> <source_hook> [excerpt] writes <epoch>-<skill>-<hash>.json atomically (tmp-then-rename) with fields queued_at, queued_epoch, skill, reason, context.excerpt (truncated 500 chars), ttl_hours (default MTK_QUEUE_TTL_HOURS=24), source_hook. The dispatcher's drain sorts newest-first, expires entries past their per-entry TTL, dedups by skill|source_hook, caps 3 per turn, and bumps queue_writes/queue_drains/queue_expired in .claude/analytics.json. Everything is gated by mtk_queue_enabled (MTK_HOOKS_TIER2, default 1); when 0, writers and dispatcher no-op. Per S3.13 tier-2 must never modify source code. The queue is workspace-local and gitignored.
Why it worksInline nudges can't defer or survive compaction/session end, and multiple UserPromptSubmit hooks have no guaranteed order. A durable, deduped, TTL-bounded queue lets any hook safely propose a skill for later while keeping the agent in control and giving engineers a per-machine kill switch.
#scripts/build-rule-index.sh
script
Regenerates .claude/rules/INDEX.md from rule frontmatter; --check fails CI on a stale index.
Trigger bash script — bash scripts/build-rule-index.sh after editing any rule file; CI runs bash scripts/build-rule-index.sh --check
What it doesThe deterministic generator behind the wake-up layer. It reads each rule file's frontmatter and content and rebuilds the INDEX.md table so the always-on layer never drifts from the actual rules. A --check mode lets CI fail the build when someone edits a rule but forgets to regenerate the index.
Example$ bash scripts/build-rule-index.sh
Wrote .claude/rules/INDEX.md (23 lines, budget 60)
If an engineer later edits a rule's frontmatter and commits without regenerating, CI runs bash scripts/build-rule-index.sh --check and fails: STALE: .claude/rules/INDEX.md does not match current rules — run scripts/build-rule-index.sh (exit 1).
How it worksFor each .claude/rules/*.md (skipping INDEX.md), awk extracts the axes: block (decision / topic / scope) and the paths: glob list from frontmatter, greps the first H1 for the title and the 'Cite rules as <prefix>' line for the rule-id prefix, counts matching - **<prefix>. rule lines, and runs wc -l for the line count — then emits the markdown table. BUDGET_LINES=60 triggers a non-fatal WARN if the generated index is longer. --check regenerates to a temp file and diffs against the committed INDEX.md, exiting 1 if they differ or the file is missing.
Why it worksA hand-maintained index rots the moment a rule changes, and a stale wake-up layer silently misroutes rule loading. Generating it deterministically and gating on --check in CI keeps the cheap always-on layer trustworthy.
#scripts/mtk-compress.sh
script
stdin→stdout filter that compresses noisy build/test/JSON/HTML output before it lands in context.
Trigger bash pipe — <command> 2>&1 | bash scripts/mtk-compress.sh <mode>; bash scripts/mtk-compress.sh stats for savings
What it doesA pipe-through filter that shrinks long, repetitive command output — build logs, test runs, JSON dumps, fetched HTML — before it reaches the model's context, keeping the diagnostic signal (failures, summaries, timings) while dropping the repetition. It has explicit safety carve-outs for content that must never be summarized away.
Exampledotnet test 2>&1 | bash scripts/mtk-compress.sh tests
A 48,000-char test log collapses to ~1,800 chars — every FAIL/Error line and the summary survive while runs of identical PASS rows are elided. Each run appends one JSON line to .claude/observability/compression.jsonl, and bash scripts/mtk-compress.sh stats later reports all-time and current-session savings.
How it worksFive modes: auto (detect by content shape, default), json (truncate arrays past N items to head+elision+tail, clip long strings), logs (keep first/last N lines, insert an elision marker), tests (preserve FAIL/Error/summary, collapse PASS noise), html (strip script/style/comments, collapse whitespace, truncate). Tunable via .claude/settings.local.json env block: MTK_COMPRESS_MIN_CHARS (1000), MTK_COMPRESS_MAX_LOG_LINES (30), MTK_COMPRESS_MAX_ARRAY_ITEMS (5), MTK_COMPRESS_DISABLED (0 → pass through), MTK_COMPRESS_WARN_CHARS (5000), MTK_COMPRESS_MONITOR_DISABLED (0). Safety carve-outs — never route secrets/tokens/keys, security/auth findings, migration/destructive-operation output, or audit/compliance content through it (compressing a leak does not contain it); compress only the surrounding noise.
Why it worksBuild and test commands routinely emit 20-60k chars where 95%+ is identical PASS rows or progress lines, burning context budget without proportional information. Head+elision+tail reclaims thousands of tokens per command while failures, summary counts, and timing markers stay intact.
#hooks/compress-monitor.sh
hook
PostToolUse hook that warns when large Bash output skipped the compressor.
Trigger automatic PostToolUse hook
What it doesA monitoring hook that runs after a tool call and flags large Bash command outputs that did not go through mtk-compress.sh. It makes compression opt-in but visible — nudging the engineer when a big, uncompressed output just burned context budget.
ExampleAn engineer runs a Bash command that dumps a ~60,000-char log straight into context without piping it through the compressor. After the tool call, the PostToolUse hook emits a {"continue": true, "additionalContext": "💡 mtk-compress: Bash output was 60,000 chars (warn ≥ 5,000). Pipe long output through bash scripts/mtk-compress.sh logs …"} nudge, picking the suggested mode (tests/logs/html/json/auto) from the command shape.
How it worksFires as a PostToolUse hook, reads the JSON payload on stdin, and warns only when the tool was Bash, the result exceeds MTK_COMPRESS_WARN_CHARS (default 5000), and the command did not already contain mtk-compress (or MTK_COMPRESS_DISABLED). It can be silenced per-machine with MTK_COMPRESS_MONITOR_DISABLED=1. It only warns — no blocking, no mutation of the tool result. Requires python3 to parse the payload.
Why it worksCompression only helps if it's actually used; a silent miss means budget is wasted with no signal. The monitor turns 'forgot to compress' into a visible, correctable nudge instead of an invisible cost.
#hooks/context-budget.sh
hook
PostToolUse hook that tracks per-session read/edit/op counters and nudges a deliberate reset/handoff before context degrades.
Trigger automatic PostToolUse hook (registered in .claude/settings.json)
What it doesMaintains per-session, per-project-per-day counters (unique files read, modifications, total operations, bytes read) in a temp file and prints advisory CONTEXT BUDGET nudges as thresholds are crossed. It is the deterministic backstop behind the context-engineering budget guidance — purely advisory, it never blocks.
ExampleThe script fires four warnings, once each per session: 30+ unique files read → 'Consider narrowing focus…'; 40+ modifications → 'Consider committing a checkpoint…'; 120+ total operations → 'If switching tasks, capture state with the handoff skill first'; and when the read-bytes token estimate (bytes_read/4) crosses MTK_CONTEXT_BUDGET_PCT% (default 60) of MTK_CONTEXT_WINDOW_TOKENS (default 200000) it prints 'estimated ~N context tokens consumed … past 60% of the 200000-token window. Reset deliberately before quality degrades: capture state with the handoff skill and start a fresh session.'
How it worksRuns as a PostToolUse hook, sources hooks/lib/hook-io.sh, and holds a session lock across load→modify→save so concurrent firings don't lose counter updates. It counts by tool type: Read accumulates unique file paths and bytes read (capped at 100k per file), Edit/Write increment mods, Bash/Glob/Grep increment ops. The token figure is a read-bytes FLOOR (bytes_read/4) that under-counts because it cannot see assistant output or non-read tool results. Tunable via MTK_CONTEXT_WINDOW_TOKENS and MTK_CONTEXT_BUDGET_PCT; advisory only, never blocks.
Why it worksA session can ride the budget up to a forced mid-edit compaction with no warning. The 60% nudge is the hard floor that fires before that, and pairing it with context-engineering's ~40% proactive reset gives a two-stage guardrail; keeping it advisory avoids interrupting legitimate long sessions.
#hooks/post-compact.sh
hook
Post-compaction hook (SessionStart matcher "compact") that re-injects on-disk state (tech stack, active spec/plan, incomplete tasks, handoff, active workflow) after auto-compaction.
Trigger automatic SessionStart hook with matcher "compact" (registered in .claude/settings.json and hooks/hooks.json, per rule S3.11)
What it doesAfter Claude Code auto-compacts a session, this hook scans the repo for durable state the compacted conversation may have lost and emits it back as a recovery context block. It is the mechanism that makes handoff artifacts and workflow artifacts actually survive compaction — the write-side skills persist state, this hook points the fresh context back at it.
ExampleAfter compaction the hook reads .claude/tech-stack (+ tech-stack-pm), the most recent docs/specs/*.json and docs/plans/*.md touched in the last day, any unchecked [ ] items in tasks/todo.md, the newest docs/handoffs/*.md, and any .mtk/workflows/*.json with "status":"active" (surfacing workflow type, phase_cursor, and last event). It prints one line: {"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "POST-COMPACTION RECOVERY: … Active workflow: wf-… (type=BUILD, phase=phase-3, last_event=…) — read .mtk/workflows/wf-….json before advancing; do not re-init."}}.
How it worksRuns as a SessionStart hook with matcher "compact" (there is no PostCompact event). No external dependencies — pure bash file scanning plus parameter-substitution JSON escaping (no jq). It emits a SessionStart hookSpecificOutput.additionalContext envelope only; no blocking, no side effects on the session. For an active workflow it explicitly instructs the orchestrator to read the artifact before advancing and not to re-init.
Why it worksAuto-compaction silently destroys chat-resident orchestration and session state; the durable artifacts written by handoff and workflow-artifacts are only useful if a post-compaction session is pointed back at them. This hook closes that loop automatically instead of relying on the model to remember to re-read.