How it works
lucid is a set of Claude Code hooks around a few plain files. The hooks feed the dream into every agent and enforce claims, so the protocol doesn’t depend on agents remembering to follow it.
Scroll sideways to see the whole diagram.
The hooks#
| Hook | What it does |
|---|---|
SessionStart (also after /compact), SubagentStart | Injects the plan, the live memory and active claims, within a fixed budget. Subagents and Agent-Teams teammates start with the full shared context instead of a one-paragraph brief. |
| UserPromptSubmit, PreToolUse | Injects only what other agents wrote since you last looked. When nothing changed it injects nothing and costs no tokens. |
| UserPromptSubmit while workers are busy | If other agents hold active claims, adds one line telling the lead to write a chat change of plan into .lucid/plan.md first, so running workers get it and it survives /compact. |
| PreToolUse on edits | Blocks a write to a file another active agent has claimed, whether it goes through Edit/Write or through Bash (>, sed -i, cp/mv, scripts that open(…, 'w')). |
| PreToolUse on Bash | Adds the calling agent’s real id to every lucid command, so every entry records who wrote it and agents never have to manage ids. |
| PostToolUse on Edit/Write | When an agent edits .lucid/plan.md, records the change as a plan diff under that agent’s id. |
| SubagentStop, TaskCompleted | Asks once for notes from an agent that edited files but logged nothing. A stopping subagent’s claims are released. |
| Stop, SessionEnd | Releases a session’s claims when its turn or the whole session ends. An agent between turns isn’t editing, so its claims shouldn’t block anyone. |
Every hook goes through a tiny shell guard first. It walks up from the project directory looking for .lucid/, stopping at the repo root. If the project hasn’t opted in, or python3 isn’t available, it exits without starting Python, so projects without lucid pay a few shell builtins and nothing more.
Digest and delta#
lucid injects context in two sizes.
The digest is the whole dream, given when an agent starts (and again after /compact): the plan, the consolidated digest.md if one exists, active claims, and the newest log entries that fit a fixed budget. Older entries are left for lucid log. This is why decisions survive compaction: in a real run, after /compact the lead quoted a log entry word for word, from the re-injected dream rather than the summary. You can print the digest yourself with lucid status.
The delta is given before each prompt and each tool call: only entries that other agents wrote since this agent last looked. When nothing changed, nothing is injected and it costs no tokens. When the delta includes a plan change, it also says that the plan is the user’s latest intent and overrides the agent’s original brief.
[lucid] 1 update(s) from other agents since you last looked:#3 PLAN CHANGED by user: - add a one-line docstring to every function in calc.py+- ALSO add multiply() to calc.pyPlan edits made outside Claude’s tools, such as in your editor, with sed or by a git checkout, are picked up too and attributed to user.
Claims and TTL#
Before editing, an agent claims a set of files with lucid claim 'glob' --for "task". While the claim is active:
- Other agents’ writes to matching files are blocked, with a message naming the owner and the task.
- Another agent’s
lucid claimthat overlaps it is refused. - Every agent sees it under “Active claims” in its digest.
A claim ends when:
- its owner runs
lucid release; - its owner is a subagent that stops, or a session whose turn or session ends;
- its owner has been silent for
LUCID_CLAIM_TTLseconds (default 1200).
Releasing at the end of a turn or session came out of testing. In the benchmark, the first memory run failed 1 of 4 because a finished session’s claim blocked the next session. In a live split-pane Agent Teams run, teammates, which are separate sessions, sat idle while still holding their claims.
⚠stale facts#
Facts are logged with evidence such as -e src/auth.ts:42. lucid records a hash of each cited file. If the file changes afterwards, the entry is shown with ⚠stale, telling agents to re-read the code before relying on it. /lucid:dream re-checks stale entries and retires the ones that are no longer true. In a real run, a worker logged a note before it had finished; lucid flagged it ⚠stale because the cited file had changed since, and the lead pointed out the inaccuracy.
The stop gate#
When a subagent stops or a task completes, lucid checks whether the agent edited files but logged nothing. If so, it asks the agent, once, to record its decisions, dead ends and non-obvious facts before finishing. Changing the plan counts as reporting. A stopping subagent’s claims are then released.
Bash write detection#
Agents don’t only edit through Edit and Write. In a real Agent Teams run, a teammate edited through a Python heredoc in Bash, which skipped claims and the stop gate. lucid now inspects Bash commands for file writes and applies claims to them too. It catches:
- redirects (
>,>>) andtee; cp,mvandrm;sed -i;- Python writes, such as
open(…, 'w').
This is a heuristic. It doesn’t catch every way a program can write a file. See Limits.
An append-only log#
Agents only add to log.jsonl; direct edits to it are blocked. To retire a wrong entry, agents use lucid supersede, which adds an entry instead of rewriting history. In git, the log is merged with merge=union, so two people appending never conflicts.
Agent ids#
Every entry records who wrote it. lucid stamps each agent’s real id onto its lucid commands, so agents never manage ids. This was added after leads were seen inventing worker ids. When you run lucid yourself, your entries appear as human.