Repository navigation
levain 0.11.0
Added
-
Decisions waiting shows agents' consent requests, read-only. A Claude Code or Codex tool call waiting on you now appears under "Agents waiting on you" (counted, first) with the agent, the tool, the request as filed in a monospace block, its working directory and targets, a live countdown, and a copy button for the
levain consent answer ... allow|denycommand to run in a terminal; requests decided in the last day list under Resolved with how they were answered. The request's details (what it asks, its working directory and targets, the answer commands, how it was answered) need the page's token on every bind, loopback included: the decisions panel itself carries only the agent, the tool, the tier, the state and the times, and a page opened without its token says it carries no token, so request details are not shown. A decided request shows its state and times from the panel row without a request fetch; its "details" button fetches the request and shows how it was answered (the audit line), and Resolved lists the newest decision first. A request whose input is too long to show offers no commands and says to answer it in the agent's own dialog. The page reads the queue without opening it for write and answers nothing; it reads at most 200 waiting requests (the soonest to expire; past that it says there are more than 200) and the last 50 decided ones, and a row whose seal or timestamps do not check out is skipped and counted. Known edge: a decided request's state and the key that answered it are shown as the queue row stores them, not re-verified against the signed answer. -
A Sessions tile on At a glance. It counts the agent sessions (Claude Code and Codex) whose levain hooks fired in the last 15 minutes and opens into the focus view: Active, then Quiet (15 minutes to 24 hours; older sessions are not listed). Each row names the agent and the working directory's last folder, when the session last fired a hook ("last active 3m ago"), Claude Code's sandbox as the settings files configure it for that directory ("sandbox configured on (project settings)", "sandbox configured off"; Codex says "sandbox not read"), whether the hook delivered the entity's memory at session start ("memory loaded at start" or "memory not loaded"; nothing when unknown) and how many consent requests that session has waiting. When two listed sessions share the agent and the folder, each of those rows also says when it started ("started 40m ago"); two whose starts round to the same minute (past an hour, the same hour) still read alike. The view's footer repeats what the settings readers cannot see (
CLAUDE_CONFIG_DIRrelocation,--settings, server-managed settings). It reads this install's.levain/sessions/only, the newest 50 records, each at most 16 KiB; a file that is not the registry shape is skipped and counted. The tile and panel carry no session id and only the folder name, never the full path. -
Known edges, Sessions: (1) the session files are a display, never an authority. Hooks write them as your own user, so any process running as you can forge one; nothing in levain reads them to decide anything. (2) The words say what was measured: no session-end hook is installed, so a silent session goes Quiet, never "ended", and "last active" is the last hook event, never proof the agent is running; sandbox is "configured", never "confined". (3) Only this install's sessions are shown; a session under another install does not appear (the documented default is one global install). (4) Two hooks firing for one session at the same moment can lose an update, so the memory word may be missing for it and "last active" can step back by seconds; nothing wrong is shown. (5) Past 200 waiting consent requests a session's waiting count can read low.
-
A session's transcript, from the Sessions view. Each session row has a "transcript" button that shows, oldest first, what levain's own hooks recorded for that session: your prompts, the agent's final reply each turn, each tool it ran (the tool's name, plus a shell command's first line, a file or notebook tool's path, or an
apply_patch's first file), and the consent requests that session filed with how each was answered. Above it is the command that reopens the full session in the agent itself, with a copy button:cd -- <dir> && claude --resume <id>orcodex resume <id>. The vendors' own session files are never read. The transcript names the session id, the full working directory and your words, so it needs the page's token on every bind, loopback included; the Sessions panel itself still carries none of them. -
What this keeps at rest. Claude Code and Codex installs now run more levain hooks,
StopandPostToolUse(and on Claude CodePostToolUseFailure, so a tool that ran and failed has its line too); they only record and print nothing. They write<install>/.levain/sessions/<id>.log.jsonl(owner-only, already git-ignored) beside the session file: each text cut at 4,000 characters, each log kept near 256 KiB by dropping its oldest half, the newest 200 sessions kept. Your prompts and the agent's replies are in it as written. Delete.levain/sessions/to clear it. A Codex install shows Codex's "Hooks need review" screen once after this update, because its hook list changed. -
Known edges, transcript: (1) it is a display, never an authority: hooks write it as your own user, and nothing in levain reads it to decide anything. (2) A tool the agent was refused leaves no tool line (the agent fires no PostToolUse for it); a refusal that went through levain's consent queue shows as its consent line. (3) If a hook cannot take the sessions lock within about a quarter second, that one event is not recorded. (4) A symlinked
.levainor.levain/sessionsdirectory is followed, as the Sessions view does: whoever can plant it already owns the install. (5) The hooks' locking and no-follow opens assume macOS or Linux, the platforms levain supports. -
An At a glance tile that has more than fits opens all of it. Health, Ops and any other glance tile that shows "+N more" is a button like Decisions waiting: a click (or Enter) opens the focus view listing every item, each metric with its status, value and unit, with a filter box once there are eight or more. No glance tile draws past its own edge any more: each draws as many items as fit its height on the desktop and the phone, and the "+N more" line is always whole.
-
Decisions waiting and Sessions open with a filter box. Their focus views carry a filter from the first row, not only past eight: Decisions narrows by the agent and tool, the request's state word and its tier; Sessions by the agent, the folder and the row's chips ("1 waiting", "memory loaded at start"). It filters what the view already shows; no panel gains a field.
-
The cockpit has a Team zone, read-only. When your install belongs to a team ledger, a fourth zone beside Work, Mind and Identity shows what the ledger says: Members, Waiting on you, Where agents were stopped, Held by nobody, In force and Joins pending, plus a Sync card (the remote, the last fetch and its error, integrity problems, canon status). It reads the ledger at most once per refresh and fetches nothing. A solo install shows one card saying it has no team ledger; a ledger this machine cannot read shows one card naming why, in words (git's own output stays out of the page and in
levain team status); a ledger only partly judged here shows every pane with the reason beside them. Members lists each member by handle, the owner first: their role, whether a signing key is in force, waiting for them to confirm it, or missing (a member with no key cannot sign), and since when they hold the role when the ledger records it. Joins pending lists each proposed key that waits for its member to runlevain team key confirmon their own machine (with its fingerprint) and an ownership offer that waits forlevain team accept. No teammate's email address is shown. -
The Team zone's cards size to their rows. On the desktop each Team card is as tall as its rows (its usual height is the cap, past which it scrolls) and the cards in a row line up at the top, so a pane with three short rows no longer leaves a tall empty card. Anywhere in the cockpit, a date in a row's text (2026-10-15) no longer breaks across two lines at its hyphens.
-
The Team zone has two actions: Fetch now and accept. Fetch now on the Sync card fetches the team ledger from its remote and rebases onto it, once, on bounded waits; it never pushes, and it does what the team hook already does on its own every interval, so it fires on one click in every install (a standing policy may still raise it). Accept on Joins pending appears only while a join waits on this machine: this machine's own key proposed for a member, or ownership offered to this machine's member. It brings that key into force or takes that ownership, the team's trust-root acts, so it is T3: it fires only on a
laptop-presencesignature over the cockpit's stored record, never on the page's token (the cockpit server holds the member's signing key, which an agent's sandbox does not, so a token-only accept would let any holder of the token act as the operator). What is signed is the kind, the member's handle and a digest of exactly the offer or proposed key the row showed; when the cockpit applies it, it fetches first and refuses anything that changed since the signature, and an act already done some other way is refused as changing nothing. A push that fails, a busy ledger or a missing signing key leaves the signed accept waiting and retried, never spent. The page itself cannot yet make alaptop-presencesignature, so the row names the laptop command that does the same act (levain team key confirmorlevain team accept). -
Known edges (Team actions): a fetch can hold the request for longer than one git call's 20 s bound (it runs several), and answers "busy" while the team hook's own fetch holds the ledger; a git read that hangs for a minute is replaced by a new one without stopping it; an accept whose fetch keeps failing for a permanent reason (the remote's branch deleted) is retried until it expires; the refusal reason of an accept the ledger moved under reads only "refused" in its outcome.
-
Known edge: the Team zone reads the team of the install the cockpit serves (or the start you pass an embedder's
build_default_cockpit); a cockpit with no install root shows no Team zone. A team read that hangs shows the last good read with its age after a minute. -
The cockpit's Team control is gone; the Team zone replaces it. The cockpit no longer lists running
levain team viewservers or links to them:/team_views.jsonanddashboard_team.jsare removed, andlevain team viewno longer publishes an entry under<levain home>/team-views/(entries left there by an older version are never read again; delete the directory if you like).levain team viewitself stays, for a clone with no levain install to serve a cockpit from. -
levain doctorchecks anneal's consolidate baton. A new "consolidate baton" row fails when the store needs the baton and it is unclaimed, unreadable or held by another session, and the MCP launcher row fails a launcher with no--session-id(it nameslevain update) or one serving as a session other thanlevain. -
Codex's consent hook, as a library; no
levaincommand installs it yet.levain.consent.adapters.codexfiles Codex'sPermissionRequeston the consent queue and tells Codexallowordenyonly for a signed answer or a standing grant the queue verified; anything else prints nothing and Codex shows its own approval prompt.install(workspace)writes the hook into the workspace's.codex/hooks.json, keeping your other hooks. Levain never trusts the hook for you: Codex shows "Hooks need review" at its next start and you trust it there.trust_status(workspace)asks Codex itself whether the hook is trusted, untrusted, changed or disabled. Anapply_patchinto Codex or Claude Code config or hooks is filed for you alone, like any other agent-config edit; a patch whose file headers cannot be read is treated as one. -
levain consent canary --agent claude_code|codexruns one short real agent turn in a new, empty scratch directory and passes only if the installed hook fired and its denial reached the agent (Codex runs under a terminal). It refuses a directory that is not new or empty, so it never touches a real project's hooks. Known edge: the Codex canary runs Codex interactively under your ownCODEX_HOME, so Codex may record its own interface state in~/.codex/config.tomlas any Codex start does; the canary writes no hooks or trust there. -
The consent hooks pin the agent versions they were tested on (Claude Code 2.1.300 and 2.1.301, Codex 0.162.1). Installing either hook (
claude_code.install,codex.install) on another version of its agent is refused unless you passallow_untested_agent_version=True; the canary installs past the pin, since it checks whatever version is installed;levain doctorwarns when an installed consent hook meets an untested version, a Codex trust state other than trusted, a hook script that is not levain's, or a settings entry that names a missing script. -
levain consent pendingandlevain consent answer <id> allow|deny --signing-key <key>: answer a waiting consent request from the terminal.pendinglists each open request, oldest first: its id, agent, tool, working directory, the paths it targets, its tier, when it was filed and how long its hold has left.answerfirst prints the request as the agent filed it (the tool, its input with secret-named keys masked, the working directory, the tier, the input digest and the request digest it signs), every agent-supplied text with its control, format, bidi and line-separator characters printed as\uXXXXescapes (inpendingtoo), then signs the answer with your key in thelevain-consent-answernamespace and hands it to the queue, which verifies it against a fresh replay of the authority log (an enrolled, unrevoked key; for an allow, a key class that covers the tier, so an edit of agent config or hooks needs a laptop-presence key) and records it; the hook waiting on the request then delivers it. A deny is signed too. One request, one signature: a second answer, an unknown or no-longer-pending id, an unenrolled key or a key class that cannot allow the tier is refused with the queue's own reason; an unknown or no-longer-pending id is refused before anything is signed, and Ctrl-C at the passphrase or touch prompt prints "answer cancelled" and records nothing.--queue-dirnames the queue (default$LEVAIN_HOME/consent, else~/.levain/consent). The answer is verified against the cockpit the hook for the request's agent verifies against (cockpit_data_rootin the queue directory'sclaude_code.jsonorcodex.json);--data-root/--pathnaming another cockpit is refused before anything is signed, and when the queue names none, one of them is required; a root that holds no cockpit is refused before anything is signed too. -
A consent request's display masks secret-named keys by the words inside them. A key is split into words at
_,-and camelCase, and masked when password, passwd, passphrase, secret, token, authorization, credential, cookie or bearer occurs inside one of its words, or apikey, privatekey or accesskey inside two adjacent words joined, or when a word is or ends inauthand the next word is not one that says what kind of thing it is about (url, type, method and the like):api_key,accesstoken,secretKey,DB_PASSWORD,auth,basic_auth,basicauth,X-Authandauth_headerare masked;author,auth_url,authTypeand aprivateflag are shown. A key that only looks secret-named is masked too (max_tokens,tokenizer): a secret shown costs more than a count hidden. A masked value shows its type, and a string its length:[redacted: str, 24 chars]. This changes the display of requests filed from now on; requests already filed keep the display they were sealed with. -
Known edge:
levain consent pendingandanswerprint an agent's control and format characters as\uXXXXescapes but leave spaces and backslashes as they are, so a working directory that literally contains the text\u000areads like an escaped newline. -
Known edge: when the queue directory's adapter settings name no cockpit, the hook verifies no answer, so an answer given with
--data-rootis recorded and never delivered; the agent's own dialog answers it. -
levain cockpit pendingandlevain cockpit sign <pending_id> --signing-key <key>: sign a waiting cockpit write from the terminal.pendingreads the cockpit journal (no server, no token) and lists each waiting pending, soonest expiry first: its id, verb, tier, panel and row, who requested it, when it was issued and how long it has left.signreads the pending off the journal, prints its record in full from the very bytes it signs (every field of the signed record, each effect field by field, and the record's sha256; then, under their own heading because they are not signed, who requested it and your key's fingerprint and class), signs them in thelevain-cockpitnamespace, and posts the signature to the runninglevain serve --writeat--url(defaulthttp://127.0.0.1:7420, loopback only), which verifies it over its own stored copy, decides and applies; the server's answer is printed as it gave it. The signature goes to--urlonly: no proxy setting is used and no redirect is followed (a redirect is printed as a refusal). The write token comes from$LEVAIN_WRITE_TOKEN, else a prompt, never an argument. Refused before anything is signed: a pending that is not waiting or has expired, a reported pending, a key whose class does not cover the pending's tier (the decision would otherwise end the pending refused), nothing listening at--url, a--urlwithout a usable port, and no token or one that cannot be sent as a header. The exit code is 0 only when the server answers a fresh allowed decision that committed and the journal you named, read again, no longer shows the pending waiting; otherwise the CLI prints the server's answer and what the journal shows, and exits 1 (a replay of one already decided exits 1). -
Known edge: anyone running as your user with the server's write token and a usable signing key can run
levain cockpit sign; a decision's authority rests on the key's custody, as forlevain consent answer. -
Known edge:
levain cockpit signsends the write token and the signature only to a listener your own OS user holds on--url(on macOS one bound to exactly that address; on Linux, that address or a wildcard bind of the port), so another user's process on the port is refused before anything is sent; it does not check that the listener is the server holding the journal, so a process running as your user on that port receives them, as with any loopback token. A timeout or a dropped connection after the request was sent says the server may have applied it, never that nothing was decided. It cannot make the command report success, since that needs the journal to show the pending decided.pendingandsignopencockpit.dbthe way the server does, so a zero-length file is initialized and a read-only mount fails; a missing one is refused.signreads the pending and checks the key's class in two separate reads, so a key revoked between them passes the CLI's check and the server refuses it. -
Known edge: anyone running as your user with a usable signing key can run
levain consent answertoo; an answer's authority rests on the key's custody (its passphrase, touch or presence), as forlevain consent grant create. -
Known edge: the queue keeps only a display of the tool input, cut at 2,000 characters with its keys sorted, while an answer binds the digest of the whole input. So a request whose display is cut (a long Bash command, a large Write) is not answered from the terminal:
pendingmarks it "too long to answer here" andanswerrefuses to sign it, saying the input is too long to show here; the agent's own dialog stands. The answer is signed before the queue judges the key, so an answer refused for its key (unenrolled, revoked, or a class that cannot allow the tier) still asks for your passphrase or touch. A request withdrawn or expired between the CLI's read and your signature is refused by the queue after you sign. A secret under a key whose name holds none of the masked words (auth,passphrase,pwd) is shown, as is any secret inside a free-text value. -
Standing consent grants: a signed grant can answer an agent's permission prompt for you.
levain consent grant create --agent claude_code|codex --tool <name> --workspace <dir> --posture <mode> --expires <duration|stamp> --signing-key <key>(--tooland--posturerepeatable) appends a consent grant to the cockpit's signed authority log, signed by a laptop-presence key, the same as any loosen;levain consent grant revoke <id> --signing-key <key>(any enrolled key) ends one, andlevain consent grant listshows them. A grant answers a request only when every criterion holds against a fresh replay of the log: it is not an edit of agent config or hooks; the agent and the exact tool name match (an MCP tool asmcp__<server>__<tool>; no globs or prefixes); the session's permission mode is one the grant names (a request with none matches no grant); the grant's workspace is still the directory it was signed for (its real path, with no symlink on it, is exactly what the grant names; never/, this machine's home or above it); the session's directory resolves inside it; every path the request names lies inside it with no symlink on the way: a file tool's file (Claude Code's Edit, Write, MultiEdit and NotebookEdit; Codex'sapply_patch, by its patch headers) and, for any tool, itsfile_path,path,paths,notebook_path,sourceanddestinationfields (so a Read, Grep, LS or NotebookRead grant covers the workspace only), while a path field of another shape, and a Glob pattern that is absolute, starts with~or contains.., match nothing; the tool input checked is the one the request was filed with; and the grant has not expired. A grant never names the shell (Bash, for both agents: a shell command is never parsed), never names/, your home directory, a directory above it, a symlink or a directory that does not exist as its workspace, and lives at most 30 days past the link that made it. Grants only allow; none denies. Narrowing a grant (fewer tools or modes, a workspace inside the old one, an earlier expiry) is a tighten; anything wider is a loosen. -
A grant's answer is recorded and re-verified at delivery. The queue (store format
levain-consent-queue/3) records how each request was answered (via: a signed answer or a grant). A grant answer's receipt isby: bindingand names the grant, the authority link (seq and hash) that created or last changed it, and the posture. The hook delivers a grant-answered request only while the grant still answers it in a fresh replay, and only for the authority link it recorded, so a revoke, expiry, renew, tighten or loosen before delivery takes the answer back, and a raw write of the queue store can only take a decision away. -
A grant's allow names the grant to Claude Code. It carries a
systemMessage: "levain: allowed by your standing consent grant (posture );levain consent grant revoke <id>ends it." Known edge: in a real Claude Code 2.1.300 session the operator saw only Claude Code's own "Allowed by PermissionRequest hook", not that message; which grant answered is in levain's receipt (gate by binding, the grant id and its authority link). -
Known edge: a tool's paths are read from its path fields only (
file_path,path,paths,notebook_path,source,destination, each checked when present) and a Glob'spattern; a URL, or a path the tool reads from elsewhere in its input (Grep'sglob, say), is not checked, and a tool naming none (WebFetch) is matched on the session's directory only. A relative Glob or Grep under a granted workspace can walk a symlink inside it that points outside it: the pattern's start and thepathfield are checked, not the tree walked. A target with a..component, or one crossing an existing symlink, matches nothing and goes to you; anapply_patch's path fields are checked beside its file headers. -
Known edge: the paths are checked when levain answers, not when the agent uses them: a process that swaps a path to a symlink between levain's answer and the agent's write redirects that write. Levain answers the prompt; it does not confine the agent's file operations.
-
Known edge: Claude Code's permission modes other than
defaultare taken from its documentation; onlydefaultwas observed. Codex's only accepted mode isdefault. A Codexapply_patchis matched only when the Codex adapter has been imported in that process (it registers its patch reader then); otherwise it matches nothing. -
Known edge: the Codex hook fires in interactive sessions only.
codex execnever asks for approval, andcodex -C <dir>does not load<dir>/.codex/hooks.json(Codex loads project hooks from the directory it runs in). -
Known edge: after Codex runs a command levain allowed, by a signed answer or by a grant, Codex shows no "allowed by hook" text and no grant name; the record of who allowed it is levain's receipt.
-
Known edge: a hook stopped while it waits (its agent session ended, or the hook was killed mid-hold) leaves its request answerable until the hold ends; an answer then reaches no agent, though the receipt records it as allowed (or denied). Likewise a grant revoked between levain recording a grant's allow and delivering it: the request stays recorded as allowed while the hook printed nothing and the agent showed its own prompt.
-
Claude Code's consent hook script re-runs its interpreter with
-E -Pbefore importing levain, as Codex's does, so a package planted beside it in.claude/hooks/is never imported in levain's place.-E -P, not-I:-Iwould also drop your user site, where levain may be installed. A hook installed earlier keeps the old script untilinstallruns again. -
Both consent hooks are installed as
<python> -EP <script>, so their first interpreter already ignoresPYTHON*variables (asitecustomizeonPYTHONPATHno longer runs before the hook) and the script's directory; the script's own-E -Pre-run stays for a direct invocation.installrecognises and replaces an entry in the earlier<python> <script>form, as dolevain doctorandtrust_status. Codex hashes the hook's command, so afterinstallrewrites it Codex reports the hook as changed and shows "Hooks need review" again: trust it there once more (measured on Codex 0.162.1). Claude Code ran the new entry with nothing to re-approve in the canary (2.1.300, headless). -
Known edge: both hooks run their interpreter with
-E -Pso nothing in the workspace orPYTHONPATHcan stand in for levain; a levain importable only throughPYTHONPATHtherefore never answers (the agent's own prompt stands). Twoinstallcalls racing on one workspace can interleave, and so can another program writing its settings: one that makes the settings file unmergeable during an install leaves the hook script without its settings entry (the install fails, saying why; run it again). -
Claude Code's cross-session messaging can be governed in a levain workspace (
levain init --messaging governed, opt in). Governed, the workspace's.claude/settings.json(rendered bylevain init --adapter claude-codeand bylevain updatewhere you have not edited it, and merged by the consent hook'sinstall) setscrossSessionInbound: "hold", so a message from another of your sessions shows a notice and is not delivered (a workspaceholdoutranks anacceptin your user settings), and asks for everySendMessage(permissions.ask: ["SendMessage"], plusisolatePeerMachines: true). An explicit ask rule prompts in every permission mode exceptdontAsk(which denies it), bypass included, so every send reaches the permission flow: levain's consent hook, where a standing grant onSendMessagefor the workspace can answer it and leaves a receipt, any otherPermissionRequesthook you installed, or your own dialog.install(..., inbound="hold"|"refuse"|"unmanaged", outbound="ask"|"isolate"|"deny"|"unmanaged")chooses;isolatewrites onlyisolatePeerMachines;denyturns sending and listing off (SendMessage,ListAgents);unmanagedwrites nothing for that direction. A write never loosens what the file already says: it keeps the stricter inbound value and only adds rules.levain doctorreads the controls the settings files give, with Claude Code's own precedence (managed, the last managed file setting a key winning, then user, then the workspace's sharedsettings.jsononly when stricter; a deny or ask rule counts when it names the whole tool, as a bare name or a glob such as*;settings.local.jsonis not credited, since it can only tighten and where Claude Code keeps it depends on worktrees and ownership) and warns, naming each direction, when one is not governed; the hook pin now covers each settings file's messaging controls, so removing theholdor the ask rule shows as a change, while an install pinned before this version is not flagged for controls it never set. -
Known edge:
SendMessageis one tool for other sessions, subagents and agent-team teammates, so the ask rule also asks before Claude resumes a subagent or messages a teammate; a standing grant onSendMessagefor the workspace makes that quiet. Without one, aclaude --bgsession nobody is attached to waits at that prompt until someone answers it, so sessions that message each other stall, and in aclaude -prun every send is denied. The ask-rule stall applies to a workspace that chosegoverned(below); under the defaultisolate, a send beyond this machine also waits at itsisolatePeerMachinesprompt. Nothing documented lets a hook see a held message, so the queue cannot release one: remove the workspace'scrossSessionInboundto receive messages (nothing outside the workspace loosens it). Doctor reads files, not the running Claude Code: it cannot see--settings,--setting-sources(a session that loads no project settings has neither these controls nor levain's hooks), a relocatedCLAUDE_CONFIG_DIR, MDM-profile, server-managed or (on WSL) inherited Windows settings (one withallowManagedPermissionRulesOnlydiscards the workspace's ask rule), or a plugin mod that approves calls (tool.check, which can answer an ask rule); Claude Code before 2.1.224 does not know these keys, and before 2.1.248 ignores a value it does not recognise. A session started in a directory below the workspace does not read the workspace's sharedsettings.json. -
levain init --messaging isolate|governed|off|unmanagedpicks the workspace's messaging posture; the default isisolate(claude-code adapter only; any other adapter, and--web, are refused before anything is written).isolate, the default, writes onlyisolatePeerMachines: true, so Claude Code asks before a send leaves for a session beyond this machine, even inbypassPermissionsmode; levain neither queues nor records a send to a session on this machine and holds no incoming message (Claude Code's own default decides per message), andlevain doctornames those gaps in an OK row.governedwrites the three keys above.offrefuses inbound (crossSessionInbound: "refuse") and deniesSendMessageandListAgents.unmanagedwrites none of the keys. Whichever is chosen, Claude Code never counts a message from another session as your consent. The choice is recorded asmessagingin<install>/.levain/config.json, andlevain updateandinit --forcerender it again;initrecords the choice, the default included; an install with no record is read off its own.claude/settings.json(a deny onSendMessagereadsoff; a hold, refuse or ask readsgoverned; none readsisolate), so a re-render never drops a hold or an ask the file carries, and a recorded value that is not a choice, or aconfig.jsonthat cannot be read, rendersgoverned. Doctor warns when the files do not match the recorded choice (a recordedisolatewith noisolatePeerMachines, a recordedgovernedoroffthat is not governed). The settings template itself carries none of the keys: the renderer adds them, afterhooks. -
Known edge: moving an existing install to another posture re-runs
levain init --path <install> --force --messaging <choice>; there is no lighter verb for it. A dashboard edit racinginitcan drop the recorded choice; the nextlevain updatethen reads it off the workspace's settings file, which keeps a governed file governed but turns a recordedunmanagedback intoisolate. -
levain doctorsays whether the session transcript hooks are wired. A Claude Code install gets a row for Stop, PostToolUse and PostToolUseFailure, a Codex install one for Stop and PostToolUse (only while Codex'shooks.jsonis this install's; otherwise the wiring rows already say it is not). A missing one is a warning naming it, never a failure: these hooks only record what a session did for the cockpit's transcript, so an install from before them works and records no transcript lines.levain updateadds them (run on installs made before they existed, for both agents). Known edge: the row reads the install's.claude/settings.jsonand Codex's globalhooks.json, the files levain writes; a hook you wired in another layer (settings.local.json, user settings, Codex's inline[hooks]) shows as not wired, and a layer that switches hooks off is not read here. A hook counts when its command names this install's script (the same reading as the SessionStart rows), and only from a group with no tool matcher, since a matched group records some tools only.
Changed
- levain now requires anneal-memory 1.0 (
>=1.0.0rc2,<2). anneal 1.0 lets only the session holding a store's consolidate baton consolidate it.levain initclaims the baton for the sessionlevainon every run, for a new store or an existing one; if another session holds it, init stops and says so and never takes it. The MCP launcher is now... serve --session-id levain, andlevain wrapruns as that session. An existing install picks up the new launcher and the baton withlevain update; restart your agent afterwards. Without the baton an entity records memory but never consolidates it. - Known edge: levain's anneal version check ignores pre-release suffixes (
1.0.0rc1reads as1.0.0); the>=1.0.0rc2pin keeps rc1 out of a pip-installed environment. - A cockpit row's project tag comes from its
repofacet when it has one. A provider that setsrepoon a row tags that row with that repo, in the spelling your data first used, and its words are no longer read for a repo name; so a repo named like an ordinary word (review) no longer tags a row that only uses the word but belongs to another repo. A row with norepofacet is still tagged from its words, as before. - Known edge: levain's own list panels set no
repofacet yet, so their rows are still tagged from their words. A facet the page has not registered as a repo (longer than 100 characters, or past the registry's limit) is ignored and the row falls back to its words.
Removed
levain.consent.decide_by_policy, 0.10.4's placeholder that decided nothing. A policy answer now comes only from a standing grant, throughlevain.consent.policy.guarded_policy, which the adapters call with the request, the tool input it was filed with and the session's posture.
Fixed
levain verify-hookschecks the transcript hooks and writes nothing into your install.stop.pyandpost_tool_use.pyrun from your installed copies in a temporary install root with a test event, the one transcript line each writes is checked and the root is removed. Its prompt check no longer sends a session id, which had made the real hook record a session nobody ran in the install (the cockpit listed it).levain updateandinit --forcewithout--messagingnever loosen the workspace's messaging controls. A re-render with no explicit choice keeps every control the workspace's.claude/settings.jsoncarries (a hold or refuse, an ask onSendMessage, a deny onSendMessageorListAgents,isolatePeerMachines), merged without loosening, so a lost or unreadable.levain/config.jsoncannot turn anofforgovernedworkspace into a looser one; only an explicit--messagingloosens. Doctor shows the not-governed OK row only for a choice recorded inconfig.json, and checks a recordedoffagainst the files (refuse,SendMessageandListAgentsdenied).levain updatereconciles the consolidate baton whatever else it reported, and a baton another session holds is a warning, not a failed update; a store whose status cannot be read is named rather than failing the update.- The session registry is kept like the transcript: the sessions directory is created (and tightened) to your user only, and each registry file is written 0600.
- The team accept records only what it can stand behind: a final record has the final shape (an empty or malformed entry is not taken as done), the accept marker and record are written with an fsync, a publish waits for one push round instead of six, a failure to record after publishing is a re-drivable fault, and an offer naming no known member is refused. The standalone
levain team viewsays when the ledger was judged only in part, as the Team zone does. - The cockpit's request and transcript reads have a deadline (15 s), reads queued for a view that closed are not made, request bodies are not kept after their view closes, the countdown timer stops when nothing counts down, and the board and a focus view searching the same query share one request.
- Doctor's Claude Code sandbox row and the Sessions tile's sandbox chip read the files Claude Code reads for that directory. Both credited a repository root's shared
.claude/settings.jsonfor an install or a session below the root, which Claude Code does not load there (it reads the shared file from the session's starting directory only), so a sandbox set only at the root showed "configured on" while Claude Code ran Bash unsandboxed (observed with Claude Code 2.1.301: a session started in a subdirectory wrote outside its directory; started at the root, the same write was denied). One reader now answers which project files Claude Code reads for a directory, by the vendor's rules: the shared file in that directory, andsettings.local.jsonat the repository root (a linked worktree's main checkout), unless the root is your home directory, is not yours, or there is no git repository, when it stays beside the shared file; a local file an older Claude Code left in the starting directory is read after the root's. Doctor's sandbox row, the chip and the messaging row all call it. The chip now reads the session's own directory instead of walking up to a.claudeor.git. - A folded empty card on a phone keeps its inset. At phone width its note and empty line wrapped under the title and drew flush at the card edge; they now line up with the title (seen in iOS Safari 26.2).
- Two cockpit pages open at once no longer leave each other half drawn. The server made a cockpit read wait up to 5 seconds for a free slot (past 64 waiting reads it still answers 503) instead of refusing it at once when 8 were in flight, and the page now reads at most 4 panels at a time, in order. Known edge: other routes (writes, the substrate read) still refuse at once when the server is full, but reads hold at most 6 of the 8 slots, so two always stay free for them. Waiting reads are not served strictly first-come (a page's burst finishes well inside the wait), and a client that half-closes its connection before the answer is treated as gone.
- The cockpit no longer keeps a CPU core busy while it is open. The Cognition chart's glow breathed forever, so the browser drew a frame every refresh (measured 10-15: 53 frames a second and 7-11% of a core in headless Chrome, against about 2% without it). It now breathes three times and settles; a loading dot pulses for about ten seconds, then holds still.
- A panel's action the page cannot perform draws nothing, and an action with no field is a bare button. An external panel (such as the Team Joins pane) whose own action is laptop-only or refused no longer draws an empty compose box that clipped the row naming the laptop command (the compose card of such a verb draws nothing rather than an unavailable panel), and an action with no fields (such as a fetch) draws its button alone, not an empty field bar. A text panel keeps its edit button.
- A second Codex install no longer makes doctor blame an agent for the first one's hooks. Codex reads one
hooks.jsonperCODEX_HOME, solevain init --adapter codexin install B repoints it at B. Install A'slevain doctorthen said its hook pin had "changed" and that "an agent or another program edited code that runs as you", although levain itself had made the change. Whenhooks.jsonis exactly what levain recorded writing for another install (every hook command names a script in that install'sactivation/hooks/, and that install's adapter receipt records these bytes), A's pin no longer compares it: the row is a WARN that names that install and says its scripts are not covered by A's pin. The wiring rows still FAIL, because A's Codex sessions really do run B's hooks now; they name B and give the command that takes the file back without a new interview:levain init --path <A> --adapter codex --force --answers <A>/.levain/answers.json(the old hint, plainlevain init, refuses a directory that is not empty).--forcekeeps the store and backs up the files it replaces. Two more fixes on the same path: B's own init now pins what it wrote (it used to leave B unpinned, so B's first doctor warned "trust on first use"), and A taking the file back from a newer levain re-pins (it used to stay red until--repin-hookswhenever the newhooks.jsondiffered from the first).doctor --repin-hookswhile another install owns the file says that file was not pinned. Any other change tohooks.json, including a copy that differs by a byte from the owner's record, still fails doctor on every Codex install, and so does a change to A's own settings, activation scripts or scripts its settings name. Known edges: the adapter receipt is a file your own user can write, like the pin, so a process running as you can make a plantedhooks.jsonlook like another install's write; doctor then shows the WARN and the wiring FAIL naming that directory (and says to read itsactivation/hooks/if you did not runlevain initthere), never a clean pass. The takeover hint passes your recorded answers and recorded packs when they are on disk, and says the interview runs again when there is no answers file. Doctor reads the file once for the pin row and again for the wiring rows, so a change landing during one doctor run can show in one of them only. - A pack record that is not UTF-8 is read as corrupt, not a crash.
.levain/manifest.jsonthat cannot be decoded raised out of levain's pack-record reader, solevain doctoron a Codex install (and any command reading the recorded packs,levain updateamong them) stopped with a traceback; it is now treated like any other unreadable record (found by review, reproduced by a test). - Known edges (release review): a standing grant's answer recorded in the instant a revoke lands is not delivered (the agent's own dialog stands) but the queue row reads
allowed; a queue directory made by 0.10.4 (levain-consent-queue/2) is refused by this version and its hook falls back to the agent's dialog (delete the directory); a grant matches an MCP tool's paths only by the fields it knows (file_path,path,paths,notebook_path,source,destination) and a Write or Edit grant covers every path in its workspace,.git/hooksand build files included; a relative path under a symlinked working directory refuses a grant (you are asked);claude_code.install()(the library) defaults to the governed messaging pair whateverinitrecorded; a signed team accept binds the proposal's content, so an identical proposal withdrawn and made again while the pending lives is accepted by the same signature; an accept whose act failed after its marker, followed by a manual accept of a changed proposal before the re-drive, is recorded as this pending's; a ledger read that never returns leaves its worker thread running while a new one takes over; a team discovery racing a join or a leave can briefly show the previous panes;levain cockpit signchecks the listener again after the key touch, so a listener that changes hands in the instant between that check and the request is not caught; a dashboard edit to.levain/config.jsonduringinitcan leave the recorded messaging choice different from the one rendered (the workspace's controls are kept either way); a request body read for a row on the board stays in the page's memory until a focus view closes or the page reloads. A hook refuses a symlinked.levain/sessions, but a process running as you that swaps the directory for a link between that check and the hook's next open can redirect that one write; such a process can already write those files itself. A cockpit read retried after a busy answer has its own 15 s deadline, so a retry whose body stalls can hold its connection up to about 15 s after the queue has moved on.