agentleFS
ProjectAJ14ProjectAJ14/eklavya/hooks/CLAUDE.md

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.

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
  1. Very specific:

    Run a hook by hand with EKLAVYA_HOME=$(mktemp -d) (see CONTRIBUTING.md).

    line 39
  2. Very specific:

    Run scripts/test-panel-mod.sh: it stages the mod as its own plugin, validates it, runs claude plugin test, then validates the distributed …

    line 60
  3. Very specific:

    Every hook body runs inside await run(async (input) => { ...

    line 65
  4. Very specific:

    Stop continues a turn with JSON additionalContext, not exit 2.

    line 68
  5. Very specific:

    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, …

    line 70
  6. Very specific:

    Stop closes a batch at SEAM_MIN_EVENTS (8) or SEAM_MAX_AGE_MS (20 minutes).

    line 87
  7. Very specific:

    Never wait on inference in a hook.

    line 98
  8. Very specific:

    Put developer messages in top-level systemMessage, model instructions in hookSpecificOutput.additionalContext, and the event name in …

    line 103
  9. Very specific:

    Every attempt consumes the shared max_questions_per_task allowance (default 4).

    line 136
  10. Very specific:

    Update the affected manual pages in the same PR: first-session, dials, how-it-works, memory, commit-gate and troubleshooting.

    line 155
  11. Very specific:

    When changing banner or checkpoint wording, search web/public/index.html and manual examples for quoted output.

    line 160

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

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.

EventImplementationResponsibility
SessionStartsession-startRecord 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
UserPromptSubmitprompt-submit-nudgeRefresh 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>
SubagentStartsubagent-startAsk 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-gateDeny recognized commits when the enforced session gate has not passed
PreToolUse (^AskUserQuestion$)ask-labelOn 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-contextAdd 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-toolRecord 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-quizAsk a due as-you-go question after concepts are logged
PostToolUse (`^(BashEditWriteMultiEditNotebookEdit)$`)checkpoint-quizMark 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 (`^(BashEditWriteMultiEditNotebookEditAgentTask)$`)delegate-nudgeWith 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
Stopstop-quiz-checkRecord 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.

CadencePacing and loop guard
as-you-gomin_minutes_between_checkpoints (default 4) paces single questions; Stop floors its gap at one minute. Time can re-arm Stop without new logged work.
endmin_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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.