Repository navigation
claude-hooks v1.20.0
Added
-
Ack notes in the status-line mail badge. The badge counted only
unread inbox messages, so a note left on a message the session had
sent stayed invisible until the next prompt:📬 2 ↩1now shows both
(mail:2 ack:1in ASCII), fromMailboxStore.receipt_count(), the
same predicate the announcement uses. -
Persistent task tracking (
claude_hooks/tasks/,docs/tasks.md).
Claude Code's own task list is not a record: the tools are not
offered to current models unlessCLAUDE_CODE_ENABLE_TODO_TOOLS=1,
completed tasks are garbage-collected, and backup_models' 849 tasks
had survived only in one 4.2 GB transcript. Now, on every OS by
default:- Files are the record. One Markdown file per task under
<project>/.claude-hooks/tasks/(front matter + Description /
Acceptance / append-only Log), closed ones inarchive/, and a
generatedTASKS.mdboard. Hand edits win. - Ids carry the project's prefix (
bm-42), derived once and kept
inconfig.toml; allocated by exclusive create, so concurrent
sessions never collide. - Indexed in the store the host runs (pgvector or sqlite_vec, a
borrowed connection like the mailbox's) for cross-project listing
and semantic recall; files alone still work without one. - 11 MCP tools on both memory servers (
task-create,-ready,
-list,-show,-start,-done,-wait,-cancel,-note,
-update,-link) and aclaude-hooks-tasksCLI. - Hooks: open tasks at every SessionStart (compaction included),
named and similar tasks on each prompt, a once-per-change Stop nudge
while a task is active, and a mirror of Claude Code's own
TaskCreate/TaskUpdate.keep: tasksin a disable marker. - Importer for task history in transcripts: ids kept with the
prefix, superseded descriptions preserved with their times.
backup_models imported: 849 tasks, 101 open.
- Files are the record. One Markdown file per task under
-
Deploy covers PATH wrappers and hook matchers. A new
bin/*CLI
reached PATH only wheninstall.pyran again, and a hook block on an
old matcher silently never fires.deploy.pynow adds missing
wrappers and reconciles the matchers of installed blocks
(install.reconcile_hook_matchers);verify_deploy.pyfails on
either being behind. -
Mailbox relay for cloud sessions. A Claude cloud session cannot
reach the pgvector MCP, but in the desktop app it can use a linked
local folder.hooks.mailbox.cloud_relayturns that folder into a
full mailbox client, run by the daemon (claude_hooks/mailbox/relay.py).
It is part of the mailbox on every OS: on whereverhooks.mailboxis,
with a local folder at~/claude-mailbox(the desktop app cannot link
a network share) thatinstall.pyanddeploy.pycreate.- Addressing. A session takes an alias with a
mailbox-alias
request and is<alias>@cloudto everyone else (osync@cloud).
A new cloud session for the same alias takes the address over,
with the mail waiting for it. - All eight tools, executed through the same
MailboxTools
dispatch the MCP uses, so ownership rules are identical. - Semaphore protocol. Nothing a remote session writes is read
until its<id>.semsaysready(and, when given,bytesmatches
the payload on disk). The payload and semaphore are deleted when
taken. The daemon writes its replies,INBOX.mdandstatus.json
the same way, semaphore last. - Cost. Blocked on inotify (Linux) or
ReadDirectoryChangesW
(Windows) while idle; a 30 s mtime poll only where neither works.
A request is answered within about a second; only past five passes
in 30 s (burst) does the relay batch. The database is queried only while a cloud session
has been active in the last 12 h. - Instructions.
claude_hooks/mailbox/cloud/MAILBOX.mdis the
cloud session's guide, installed into the folder byinstall.py
and kept current byscripts/deploy.py;verify_deploy.pyfails
on a stale copy. - Sandbox grant. The daemon unit runs with
ProtectSystem=strict;
deploy writesclaude-hooks-daemon.service.d/mailbox-relay.conf
granting the folder as spelled and resolved (/shared/devis a
symlink),verify_deploy.pyfails without it, and the relay probes
the folder at start and refuses to run with the fix in its log. python -m claude_hooks.mailbox.relay statusreports the folder,
the instructions and every session in it.
- Addressing. A session takes an alias with a
-
claude-consultants grant <sid> [N](POST /v1/consult/{sid}/grant)
raises a consultancy's follow-up cap by N when the user asks for more
rounds, without sending a follow-up. Before this the only carrier for
an approval was the next follow-up's--allow-extra, so a session
told "add another 6" had to hold the grant in its own memory until
then. The grant is banked on the chain root and persisted, lifts an
awaiting_approvalconsultancy back toready_to_review, and
defaults to the configuredallow_extra. The skill gains agrant
verb and runs it as soon as the user raises the limit. -
Consultants shared read cache. Within one run, results of
survey_project/list_files/read_file/glob/grepare
shared across fan-out lanes:- identical concurrent calls execute once;
read_fileis keyed on size + mtime;- an effectful tool clears the cache;
- errors are not kept.
Code:
consultants/server/tool_cache.py. -
Councils survive an engine restart. Each run checkpoints to
checkpoints.dbin its session dir; this is the default
[checkpointer] backend = "sqlite", previously unused because
production ranMemorySaver. Each run is recorded in
~/.claude/consultants-inflight.json.- Shutdown. Running councils are suspended at their next node
boundary (RunControl.request_suspend→RunSuspended). The
engine waits up toCONSULTANTS_SHUTDOWN_GRACE_S(60 s) for nodes
that are mid-call. - Startup. Every recorded run is resumed under its own sid.
Parallel lanes that had finished are not re-run, and a crash
resumes from the last completed step. Follow-ups resume with their
parent loaded from disk. A run is resumed at most 3 times. - Clients.
--waitrides out an engine outage of up to 5 min. - Deploy.
scripts/deploy.pyrestarts an engine that reports
suspends_on_shutdowneven with councils running, instead of
failing.
- Shutdown. Running councils are suspended at their next node
Fixed
-
send()stamped the process's host on the sender.from_host
came fromhost_name()rather than the caller's identity, so mail
sent for another identity was filed under the wrong outbox — invisible
to its ownmailbox-sent,-editand-cancel.send()takes
from_host, andMailboxToolspasses its own. -
Harness-started turns are no longer treated as user prompts.
Background task notifications and scheduled wake-ups / cron ticks fire
UserPromptSubmitlike typed prompts. In recent transcripts
notifications outnumber typed prompts two to one.- What went wrong. Each one got a full recall with a HyDE
expansion: over notification XML, or the same memories again on
every tick. The Stop hook stored the turn under## Prompt <task-notification>…, which later recalls surfaced as something
the user asked. The stop guard read a notification as the user's
last word. - What changed.
claude_hooks/prompt_origin.pyclassifies each
prompt from its text, then the transcript row'sorigin.kind/
promptSource, then the wakeup suffix. The hook payload carries no
source field, because Claude Code 2.1.284 compiles it out. - Notifications: no recall.
- Scheduled prompts: plain recall without HyDE. Both are
configurable underhooks.user_prompt_submit.synthetic_recall. - Stored turns: labelled
[not a user message: …]; a notification
keeps only its<summary>. - Stop guard: reads the last prompt the user wrote.
- Partially disabled projects too. A
.claude-hooks-disable
marker withkeep: memoryroutes throughhook_parts, which had
its own copy of the recall decision and kept HyDE-expanding
notifications. Both paths now calluser_prompt_submit.recall_block().
- What went wrong. Each one got a full recall with a HyDE
-
gitnexus rebuilds are supervised, and a broken index is repaired.
- What happened. The Stop hook spawned
gitnexus analyzedetached
and discarded its output, so a cut-off rebuild went unnoticed. That
rebuild left the graph database mid-write
(lbug.shadow.dirty-recoverynewer thanlbug), and every reader
then segfaulted. In opencoti, on 2026-10-01, the pre-commit
detect-changescheck crashed six times over two hours. - Supervisor. The rebuild now runs under a Python supervisor
(--supervise). It records the exit code and dirty state in
.gitnexus/claude-hooks-reindex.jsonand retries twice. - Broken index. A broken index is rebuilt on the next turn even
when nothing was edited, with a 15-min backoff after a run that
fails every attempt. The SessionStart hint warns that the index is
broken. - Docs no longer edited. The hook now runs
analyze --index-only.
A bareanalyzealso wrote a GitNexus section intoAGENTS.md/
CLAUDE.mdand installed six skill dirs in every indexed repo it
rebuilt.
- What happened. The Stop hook spawned
-
The consultants run-time safeguards were never switched on. The
stall monitor, the deadline, the live round / reroute caps and the
xauto escalator all readstate["runtime_control"]. Nothing seeded
that record, and every reader treats it as "not configured". Both
runners now seed it. Before seeding, the values were rechecked
against production data:- The hard deadlines sat at or below p90 of 279 completed councils,
so they are now 20 / 60 / 90 min for medium / high / max, plus
extra time per fan-out model. - The stall thresholds were benchmarked on toy questions and are too
tight for real calls. They now come from 6,055 production calls
(stall_defaults.runtime_stall_thresholds);POST /controlstill
overrides them.
- The hard deadlines sat at or below p90 of 279 completed councils,
-
Reasoning counts as progress in
chat_streamed. Minutes of
thinking before the first content token no longer read as a stall.
Reasoning deltas are also concatenated; previously only the last
chunk was kept. -
xauto can escalate. The runners dropped its critic, because xauto
starts on the medium base, and the escalator is wired only when a
critic is present. The productionCouncilStatealso never declared
confidence, so every critic and synthesizer score was stripped.
Both are fixed. -
Tool loops size every call. The researcher and tooled-role loops
sent nonum_predictand never compacted a growing history.
LoopConfig.prepare_payloadnow re-plans each iteration, the same
way single-shot calls are planned. -
Compaction keeps each tool result with its call. Previously the
kept tail could open on atoolmessage whose assistanttool_calls
had been elided, and OpenAI-compatible backends reject that. -
Docs:
docs/consultants-roles.mdsaid the critic runs at medium effort
and that a self-critic replaces it at low / medium. Neither is true.- It also said the stall detector truncates tool-spin, which it
cannot do. EFFORT_BUDGETSvalues are now documented as display-only.
-
scripts/deploy.pyno longer kills running councils. Restarting
the consultants engine killed any council in flight, leaving no result
and nothingreopencould rebuild; on 2026-09-29 it cost another
session's xhigh consultancy. With a council running, deploy now leaves
the engine on the old code and fails, naming the runs.
--wait-for-councils SECONDSwaits for them (re-asking every 30 s),
--kill-councilsrestarts anyway, and--dry-runshows what would
block.