eklavya / hooks
CLAUDE.md29 starsChanged 22 days agoMITView on GitHub
hooks.json registers events; run.mjs resolves the runtime and dispatches into mcp/src/hooks/. Read the root contract and mcp/CLAUDE.md before changing the TypeScript implementation. Hook schemas and observed host behavior are recorded in docs/verified-schemas.md. Ten implementations are registered across six events. The checkpoint has two registrations, for eleven rows total.
In this repository21 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- cli/CLAUDE.mdCLAUDE.md
- eval/CLAUDE.mdCLAUDE.md
- mcp/CLAUDE.mdCLAUDE.md
- skills/CLAUDE.mdCLAUDE.md
- web/CLAUDE.mdCLAUDE.md
- eklavya-dashboardSkill
- eklavya-designSkill
- verify-docsSkill
- gateSkill
- learnSkill
- levelSkill
- memorySkill
- modeSkill
- packSkill
- progressSkill
- quizSkill
- setupSkill
- skipSkill
- toolpruneSkill
- tutorSkill
The file
We split this CLAUDE.md into the rules it gives an agent
11 rules. About 0 are common, 0 are distinctive, and 11 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 39
Run a hook by hand with EKLAVYA_HOME=$(mktemp -d) (see CONTRIBUTING.md).
- Very specific: line 60
Run scripts/test-panel-mod.sh: it stages the mod as its own plugin, validates it, runs claude plugin test, then validates the distributed …
- Very specific: line 65
Every hook body runs inside await run(async (input) => { ...
- Very specific: line 68
Stop continues a turn with JSON additionalContext, not exit 2.
- Very specific: line 70
Use the shared bounded reader in mcp/src/stdin.ts: reset the idle timer on each chunk, retain the total cap, handle errors, strip BOM, …
- Very specific: line 87
Stop closes a batch at SEAM_MIN_EVENTS (8) or SEAM_MAX_AGE_MS (20 minutes).
- Very specific: line 98
Never wait on inference in a hook.
- Very specific: line 103
Put developer messages in top-level systemMessage, model instructions in hookSpecificOutput.additionalContext, and the event name in …
- Very specific: line 136
Every attempt consumes the shared max_questions_per_task allowance (default 4).
- Very specific: line 155
Update the affected manual pages in the same PR: first-session, dials, how-it-works, memory, commit-gate and troubleshooting.
- Very specific: line 160
When changing banner or checkpoint wording, search web/public/index.html and manual examples for quoted output.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Working in hooks/
hooks.json registers events; run.mjs resolves the runtime and dispatches into
mcp/src/hooks/. Read the root contract and mcp/CLAUDE.md before changing the
TypeScript implementation. Hook schemas and observed host behavior are recorded
in docs/verified-schemas.md.
Event map
Ten implementations are registered across six events. The checkpoint has two registrations, for eleven rows total.
| Event | Implementation | Responsibility | ||||||
| SessionStart | session-start | Record the session for this host process and the checkout pointer; migrate legacy config; replay, summarize and recall memory (the recalled block is shed to fit under the host's 10,000-character hook limit, counting the directive sent beside it); start a due background update; start or replace the background dashboard (dashboard-daemon.ts, never under CI or VITEST); show profile/status and supply the log directive | ||||||
| UserPromptSubmit | prompt-submit-nudge | Refresh the host session record and checkout pointer, capture the prompt, recall relevant memory, nudge a session that has not logged concepts and, with questions on and delegate_work, add the delegation line (delegation-lib.ts) next to a parent prompt of 25+ characters that is not an Eklavya slash command or a host-sent <agent-message>/<task-notification> | ||||||
| SubagentStart | subagent-start | Ask implementers to log, without asking questions; exempt the tutor. With memory on and a project that has entries, point every delegate at memory_search/memory_get, whatever the quiz switches say | ||||||
PreToolUse (Bash) | pre-tool-gate | Deny recognized commits when the enforced session gate has not passed | ||||||
PreToolUse (^AskUserQuestion$) | ask-label | On a host that paints no header chip (Claude Desktop, Cowork), deny once an AskUserQuestion headed Eklavya whose stem lacks [Eklavya], naming the fix; on any host, deny once (per session and stem, marker file in the temp directory) an Eklavya question whose options fail visibleOptionProblem (one outruns the next by a few visible words, or a description explains itself); any other question, a terminal and subagents pass untouched | ||||||
PreToolUse (^Read$) | file-context | Add the file's past observations as additionalContext, once per file per session, with a file_context receipt marked emitted after the write; parent only; adds context, and denies every whole-file Read (no offset or limit short of the last line) of a code file of 32 KB or more with history, naming code_outline and the ToolSearch query (a ranged Read always passes; types code_outline cannot outline are never held); capture-path imports only | ||||||
| PostToolUse (all tools) | capture-tool | Record one memory event with a bounded result excerpt (none for reads and edits); no quiz, summarization or provider call | ||||||
PostToolUse (mcp__.*log_session_concepts) | checkpoint-quiz | Ask a due as-you-go question after concepts are logged | ||||||
| PostToolUse (`^(Bash | Edit | Write | MultiEdit | NotebookEdit)$`) | checkpoint-quiz | Mark an edit into a git tree for only_on_changes (edit tool path, or a Bash command whose leading cd tree lists a file modified since session start); recheck pacing as work continues; no spinner on every tool call | ||
| PostToolUse (`^(Bash | Edit | Write | MultiEdit | NotebookEdit | Agent | Task)$`) | delegate-nudge | With questions on and delegate_work, tell the parent once per session, at its second distinct changed file, to hand the rest to a background agent (edit tool path, or a file git status lists with an mtime inside the Bash command's duration_ms window, in the session's tree and the tree a leading cd <dir> && named, which is remembered); stand down for good once the parent starts an agent that is not Explore, Plan, claude-code-guide, the tutor or the explainer, and mark the session as building for only_on_changes; parent only; git for at most two repositories per Bash until it fires or the parent delegates; no inference |
| Stop | stop-quiz-check | Record the turn's final message (parent only), flush the memory seam and request the remaining eligible quiz; a session that changed code and logged nothing is asked to call log_session_concepts first |
The MCP matcher accepts both standalone and plugin-scoped names. Do not narrow it to one prefix. Keep the work-tool regex anchored. Hook timeouts are 10 seconds, except Stop at 15 seconds; consult the manifest before changing them.
SessionStart keeps inside its 10 seconds with details-lib.ts: recall, the
health line and the banner's counts and level each run in their own child
(details-child-lib.ts), killed at 7 seconds from process start
(EKLAVYA_SESSION_START_BUDGET_MS), and a detail not back by then is left out.
It is a process because better-sqlite3 is synchronous: a timer cannot interrupt
a query, and process.exit waits for a worker thread stuck in one. A new
optional SessionStart detail is a child step there, never an inline query.
Run a hook by hand with EKLAVYA_HOME=$(mktemp -d) (see CONTRIBUTING.md).
Without it the run writes into the real ~/.eklavya; run() in
mcp/src/hooks/lib.ts warns on stderr when a short, synthetic session_id
meets the default home.
The quiz panel is a mod, not a command hook
hooks/panel/ is a Claude Code mod: function hooks declared under modules in
hooks.json beside the command hooks, in a runtime with no Node and no
filesystem. Fail-open applies to it exactly as to a command hook: a handler that
throws is skipped and the question stays in the database (panel_questions) for
the next sync. The rules it is validated against: static relative imports only,
every $ call inline or in a function declared at the top of the file, literal
event names, render only its own pane, and never hold the answer key (the server
grades; the mod draws and relays). On a headerless host panel_sync also carries a greeting the mod toasts once (the SessionStart systemMessage never reaches Desktop; the terminal is never sent it, and hooks/session-start.ts is untouched). It is opt-in: with quiz.panel off, panel_sync
returns {disabled: true} and the mod opens and draws nothing. A question that carries a Did You Know card (panel_questions.card, required by the present_question schema) opens on the card step: I knew this and Got it move to awaiting, I want to learn more sets learn to show the details; neither calls the server, so neither records anything. presentation in
get_session_quiz_plan is the one source for which tool asks; the checkpoint,
the Stop sweep and the delegation lines (delegation-lib.ts) read it, and an
open panel question silences the checkpoint and the Stop sweep. Read
docs/verified-schemas.md (mods section) before changing anything it records.
/eklavya-panel-demo (DEMO_COMMAND) is the maintainer's shortcut for trying the pane: it seeds PanelState.demo with a mock question and never calls the server, so it works with quiz.panel off. While demo is set the heartbeat (beat) does not sync, submit grades locally with demoResult (no panel_answer, grader or explainer) and Next restarts the mock; /eklavya-panel and a real present_question sync replace it. Only the desktop draws Client regions (VS Code has Svg but no Client, and a tree holding an element its surface lacks is replaced whole): there every answer is a numbered whole-card region from card.tsx, Teach me is a compact pill.tsx region, the Other header is a bare card over an Input of three rows, and the header mark is MASCOT_FACE as an Svg (the terminal cannot draw one, so it draws the same head from LOGO_GRID: symmetric, eyes only, square pixels that logoRows doubles sideways into half-block characters); they post {pick} or {press} to the ui.message hook, which accepts only an id of the open question, other, or teach. VS Code and the terminal keep native Buttons (VS Code numbers the labels itself, the terminal's plain Button draws 1:). The demo's Answer cards switch (desktop only) compares the whole-card default with the native buttons. Under the Teach me button the demo also draws an experiment box (experiment in register.tsx): Image with DEMO_PNG on the terminal, two Svg (demoSvg, demoSvgWithPng) on every other surface, never both, because a tree holding an element its surface lacks is replaced whole by the host's own pane.
Run scripts/test-panel-mod.sh: it stages the mod as its own plugin, validates
it, runs claude plugin test, then validates the distributed plugin.
Failures and latency
Every hook body runs inside await run(async (input) => { ... }) and returns 0.
It never calls process.exit itself. run() catches failures; openExisting()
returns null for an unavailable database and never migrates or seeds it.
Stop continues a turn with JSON additionalContext, not exit 2.
Use the shared bounded reader in mcp/src/stdin.ts: reset the idle timer on
each chunk, retain the total cap, handle errors, strip BOM, unref timers and
pause the stream when finished. Removing listeners alone leaves stdin alive.
HOOK_STDIN is 2s idle / 5s total; STATUSLINE_STDIN is 150ms / 250ms. The total
cap can truncate a slow payload; on a swallowed pipe each hook can cost the idle
timeout. test/stdin.test.ts checks bounds and processes with stdin left open.
EKLAVYA_INTERNAL_OBSERVER=1 must make both run.mjs and run() exit before
reading stdin, opening the database or spawning work. Never rely only on the
host's hook-disabling flag: recursive observer workers have occurred despite it.
Separate memory from questions
Memory runs before the quiz.enabled check. Disabling questions does not disable
history. Capture uses the lightweight capture-lib.ts path; importing worker,
provider, recall or notification modules there violates hook-isolation.test.ts.
Stop closes a batch at SEAM_MIN_EVENTS (8) or SEAM_MAX_AGE_MS (20 minutes);
SessionStart closes remaining work except on compaction, which uses the ordinary seam thresholds. Retention runs at most every six hours in
5,000-event chunks. Notification sinks share a 5s budget. Read these constants
from source when changing or documenting them.
An observer batch is queued for a detached eklavya memory process, only after
winning the machine-wide reservation and only while the queue is unpaused. A
paused queue instead gets local stand-in observations each seam and, at most
every 30 minutes, a detached --probe-paused worker that resumes only when
claude auth status or the quota cooldown says the cause is fixed. Nothing is
captured or batched outside a git checkout.
Never wait on inference in a hook. capture-tool records subagent evidence;
prompt and Stop memory seams are parent-only.
Output channels and delegation
Put developer messages in top-level systemMessage, model instructions in
hookSpecificOutput.additionalContext, and the event name in hookEventName.
Plain stdout is not a user-visible banner. Stop's context is also rendered by
the host, so keep it short and let the planner carry pedagogy details.
SubagentStart requires the JSON envelope. Check quiz.enabled, honor a
session's silence if a database exists, and continue on a missing database.
Match eklavya-tutor as a substring to cover namespaced and bare names. Unknown
agent_type receives the directive. The tutor exemption matters because the
implementer's directive says not to ask questions.
Both checkpoint and Stop return on agent_id; retain both guards even if the
host appears to deliver Stop only to parents. See docs/subagent-policy.md.
Pacing, silence and gates
quiet hides the banner and status bar, not model directives or quizzes.
quiz.enabled: false disables questions. Session-scoped silence disables that
session's questions but never bypasses an enforced commit gate; pre-tool-gate
reads it only to explain how to resume and clear the gate.
Unenforced sessions can still receive Stop questions. Enforcement skips cooldown, lifts the planner's as-you-go cap and adds commit checks. It does not override the developer's session silence.
| Cadence | Pacing and loop guard |
as-you-go | min_minutes_between_checkpoints (default 4) paces single questions; Stop floors its gap at one minute. Time can re-arm Stop without new logged work. |
end | min_minutes_between_quizzes (default 20) paces the full sweep; work-origin concept count must grow since the previous block. |
Hooks and planner must use the same clock, checking both the last block and
last answer. Stamp stop_markers or checkpoints before emitting. The Stop
block cap (default 3) and remaining session question budget also bound repeats.
Every attempt consumes the shared max_questions_per_task allowance (default 4).
Review-origin concepts cannot re-arm the work-count guard. Change matching
candidate predicates in both hooks together: both ask only work logged within
the current stretch (workSince in session.ts: activity is a prompt or a
work tool call, and a gap over IDLE_BREAK_MINUTES starts a new stretch),
newest first then last inserted, and so does the planner;
enforced gates are exempt. quiz.only_on_changes counts an edit-tool write
into any git working tree, not ignored (noteEdit, marked by the checkpoint
before its subagent and cadence exits), and a Bash command whose leading
cd names a git tree with a file modified since the session's baseline was
recorded (noteBashEdit; leadingCd is shared with delegate-nudge), before
comparing git trees.
The shell lexer in commit-lib.ts recognizes common wrappers and nested shell
forms, with deliberate misses for aliases, variables and scripts. The optional
git hook covers terminal commits; do not describe the Bash detector as complete.
Documentation and checks
Update the affected manual pages in the same PR: first-session, dials,
how-it-works, memory, commit-gate and troubleshooting. Keep the timeline
and flow labels aligned with ordering and guards. Schema/delegation changes also
update docs/verified-schemas.md and docs/subagent-policy.md.
When changing banner or checkpoint wording, search web/public/index.html and
manual examples for quoted output. Do not preserve a transcript that no longer
matches the source.
Run npm test -- hooks gate stdin hook-isolation memory-observer-guard from
mcp/; pretest builds the files the hook suites execute. Use a temporary
EKLAVYA_HOME and EKLAVYA_DB for manual probes. Behavior changes also need the
live checkpoint acceptance check in CONTRIBUTING.md; unit tests cannot prove
that the model asks a question and resumes work.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

