Repository navigation
A platform-hardening and cross-platform-reliability release. Canonical git merges are now contained in a Windows job object and quarantine a repo instead of risking corruption when a kill can't be confirmed; agent-writable surfaces across profiles, platform, setup and companion routing were tightened to least privilege; the Telegram companion's chat-scope boundaries are enforced end to end; session and MCP tokens are compared and stored more defensively; and a long pass of POSIX process, path and test-harness fixes moves the daemon's own test suite — and the behavior it tests — toward parity on Linux, not just the Windows host it's usually developed on.
Added
- Windows Job Object containment for canonical git operations. On Windows, every git call the merge path makes now runs inside a Windows Job Object via a small native launcher, so a kill reliably takes down the whole process tree instead of risking an orphaned child. Packaged (npm) Windows installs ship a prebuilt launcher; in a source checkout, a Windows
daemon_restartdeploy now rebuilds it automatically (a failed rebuild keeps the previous launcher and warns rather than blocking the deploy). When no launcher is present, merges fall back to the previous kill path. On Windows, those git calls — and the daemon's other git calls against a project's repo, including vault commits and decision-record lookups — now run git by an absolute path found on an absolute PATH entry, rather than a baregitlookup, whenever one can be resolved (otherwise they fall back to the previous behavior). This closes a path where a relative entry onPATHcould make Loom run agit.exeplanted inside a repo instead of the real git. - A configurable safety-net check for interval-ungated landings. A project that lets some merges land without the full gate (
orchestration.mergeGateInterval) can now set a human-onlyungatedLandingCheckCommandthat still runs before an otherwise-ungated landing; a red result refuses the landing without counting as a real gated pass, closing a window where an interval-skipped landing used to run no check at all. - More to see and do in the attention queue. Nearly every attention item can now be dismissed (a merge-review card with an Approve button deliberately can't be); browser notifications can be muted per notification type; and a consolidated recycle lineage, a codex isolation-gap alert for a parentless session, and a boot-stuck session all now surface there until they resolve.
- A halted manager or platform-lead recycle can recover on its own. When both ends of a recycle handoff are confirmed dead, the lineage is now consolidated at boot and the owner is paged; a new
recycle_reattempttool lets a halted handoff be retried instead of staying stuck. - Profile and grant changes are now audited for reach. Rebinding an agent to a profile that widens what it can do, changing a profile's role or
restrictedTools, and granting a capability are all recorded against who or what they actually reach. - A
vaultWritetoggle in the Profiles editor, so the gated vault-write capability can be turned on for a profile from the UI instead of a raw config patch. - Stale worktrees are easier to find and clear. A leftover worktree directory under a repo key that's no longer registered, or one renamed aside during cleanup, now surfaces for reclaim; a new human-only route clears an orphaned merge-quarantine latch (see Changed, below) by hand.
- Small session diagnostics. Each instruction file a session loads is now recorded as an event, a repeated manager submit give-up now alerts the owner, and wrap-excess decline reasons and wrap replays are now logged; and Loom now prunes
~/.claude.jsonentries left behind by deleted worktrees and deleted Loom temp directories. - The boot-dialog-stuck detector now covers more sessions, earlier. It catches managers and parentless sessions, not just workers, and now flags an unattended spawn stuck on a blocking CLI dialog even before
SessionStartfires, instead of only after. - A landing-started event is recorded before each squash, and a manager is now notified when a worker spawn it queued fails before the worker ever starts.
- The merge-gate timeout ceiling was raised to two hours (from one hour), for a project whose own test suite genuinely needs the room.
- Telegram's command menu is now split by chat type — a private chat and a group chat each see only the commands that apply to them.
Changed
- A git call whose kill can't be confirmed now quarantines the repo instead of risking a corrupted merge. The merge path kills a hung git call on a timeout; if that kill itself can't be confirmed dead, the daemon used to retry against the same repo anyway. It now quarantines the repo — no further merges or vault commits run against it — until a human clears it or the stuck process is confirmed gone; the quarantine survives a daemon restart and tracks more than one independent cause at once without losing any of them. Fixed several cases where two quarantine entries landing on the same internal key could overwrite or drop each other's record when quarantines are reloaded at startup, or where clearing one entry could delete a file another entry still depended on. A few related edge cases — around a quarantined subfolder of a repo whose own folder has since been removed — remain open and are tracked.
- Merges now pin against a recorded mainline branch instead of whatever happens to be checked out. A worker's worktree shares the project's real
.git, so a stray checkout could point a merge at the wrong branch. Solo and batched merges now verify the mainline branch against a stored watermark before and after landing (and scan from the mainline branch, notHEAD, when finding landed workers); a human-only route resets that watermark if it's ever wrong. - Merge-gate verdict correctness, continued. A transient union-merge failure is no longer cached as a rejection; a merge no longer evicts a running self-check op it could instead attach to, and that supersede decision is now made at the right moment and scoped to the exact worker that owns it; a batch-landed tree is checked against a proper three-way merge of its reviewed tip; a merge-tree conflict inside a batch is now reported as a conflict instead of silently dropped; and a candidate whose rollback fails now aborts batch assembly instead of continuing partially applied.
- Loom refuses to treat its own install directory as a project. Starting a manager, cutting a worktree, merging, or running a git or vault write against a repo that resolves to Loom's own home directory (or an ancestor of it) is now refused outright.
- Agent-facing config and profile surfaces are tightened to least privilege. An agent can no longer set a project's permission mode to anything but
acceptEdits(startupModeCyclesis now settable, by a human only), assign a profile that carriesbrowserTesting,documentConversion,allowDelta, a non-claudeharness, connections, capabilities orvaultWrite, remove a deny-list entry, set an invalid pty geometry, spawn a session that directly carries profile grants, or change a human-only config key via anunset/replace; a run-role session can no longer reach the task MCP router at all; andalertWebhookURLs are now masked on every agent-facing read and write response, and no longer retained in cleartext in config history once rotated out. - Reserved Loom-owned projects (Platform, Setup) are harder to mutate by accident or escalation. The Platform Lead and Setup Assistant surfaces now refuse renaming a reserved home, confine elevated profile binds to reserved projects, refuse a role change that would add human-set capabilities, resolve a reserved home by its stored id rather than its name, and guard every write path (including a second live Setup session or fork) consistently; a manager-role agent can no longer be minted into, forked into, or resumed in a reserved or manager-barred project, and resume/fork permission is now keyed off a session's own pinned role.
- The Telegram companion enforces chat-scope boundaries end to end. Owner-only commands and
/exportare refused on group chats; a DM-bind code can only be redeemed by the session and chat it was issued for; a binding flagged as a group or non-private chat no longer receives outbound pushes; and delivery to an unbound route, or to another session's route, is now refused rather than silently misrouted. - Control characters are rejected or stripped before they reach a guard, not after. Task titles, project-memory writes, and the remaining agent-writable prompt stores now sanitize control bytes at write time instead of after the checks meant to catch them, closing a bypass; terminal input is likewise stripped of escape and C0 control characters at the point it's submitted.
- Session and MCP tokens are handled more defensively. MCP and hook tokens are now compared in constant time; a session's MCP token is blanked out of every non-HTTP MCP server's environment, removed from the on-disk config file on Windows, and its capability-secret MCP config file is deleted once it's no longer needed; an undefined token now fails closed instead of open; MCP routes authenticate with a token minted for that session instead of trusting whatever session id appears in the URL; and SQLite's
secure_deletepragma is now enabled, so a row deleted from here on — including a revoked credential or a rotated-out secret — has its freed database pages overwritten rather than left readable. - The Platform Lead's own instruction-file reads, and codex's transcript protections, are tightened. The Lead can no longer read
CLAUDE.local.md,.claude/rulesorAGENTS.mdat its pinned instruction roots, or at any root a rebound home could leave it reading; a transcript-protected role is now kept off the Codex harness, and a Codex spawn that would otherwise drop a protection now says so loudly instead of silently running unprotected; and an unattended spawn now declines the externalCLAUDE.md@importdialog at the repo level so it can't hang waiting for an answer nobody will give. - Message delivery reliability, continued. A pasted-wrap late echo or late confirmation is now recognized for what it is instead of triggering a spurious retry; the bracket-paste writer and Enter-verify timers are now pinned to the right session generation so they can't act on a stale one; a kickoff now carries its own id and give-up-exhausted handling through the deferred-delivery path, and is held back while a boot dialog is on screen rather than racing it; mode cycles and kickoffs are bound to their own session generation; and the owner-text rank is now carried through every requeue and fails closed, rather than open, when it's missing.
- Codex reliability, continued. Codex's recent owner turns are now ordered newest-first like Claude's; a resume now restores the right archived rollout file first; engine-id rediscovery after a resume no longer picks up a sibling session's id; and the raced-owner-reply marker is only cleared for owner text actually captured after the race it guards against.
- Vault writes and auto-commit stay out of places they shouldn't touch. Auto-commit now refuses to run inside a worker's worktree, inside a registered code repo or a worktree of one, or inside
.gititself; it pauses around a canonical merge or batch fast-forward instead of racing it; a staleindex.lockis now surfaced as an alert instead of silently blocking commits; and a vault write now rejects a symlinked final target and a hard-linked overwrite target, and refuses togit initor commit an operational vault directory on the writer path too — a symlinked or hard-linked path can no longer be used to escape the vault or overwrite an unintended file. - A handful of web/gateway hardening fixes. Vault PDFs now force-download instead of rendering inline; raw vault files are served behind a sandboxing content-security policy; a gateway token's open sockets are closed the moment it's revoked, paused, rotated or deleted; the loopback websocket secret now rides as a subprotocol instead of a URL query parameter; websocket payload size and per-socket fleet subscriptions are now bounded; and a malformed terminal websocket frame is type-checked instead of being able to crash the daemon.
- Skills keep their provenance straight. A skill written by an agent can no longer pass as a later bundled version, a locked role no longer picks up an agent-written user skill, and both skill injection and manifest pruning now refuse a path that resolves outside the working directory.
- Linux and macOS compatibility, across the board. A large pass of process-enumeration, path-resolution and test-harness fixes — bounding a hung
/procread instead of leaking a thread per enumeration, comparing process start times in one clock domain, scoping the orphan reaper and its tests to real pids, and fixing a long list of path-separator, line-ending and ambient-identity assumptions — moves the daemon's own test suite, and the behavior it tests, toward parity on Linux rather than only ever having been proven on Windows. - Web polish. Chat, terminal and fleet sockets now recover after a reconnect or a dead token without a page reload; the composer draft is keyed by session id instead of bleeding across sessions; Settings, the Profile editor and the Agent editor no longer revert a concurrent agent write on save; several buttons' inline styles no longer shadow their own hover states; the vault editor gates editing on loaded content and shows a read error or a stale-while-loading state instead of a blank screen; a remote caller can now read a single task, and the board drawer shows an error state instead of hanging when a links read fails; a Request's answer dialog no longer discards a typed-but-unsent answer when a stray click lands on the backdrop or Escape is pressed — it keeps the draft for as long as the page is open and says so, and only the ✕ button (which still discards a credential secret, since that's never retained) closes it regardless; and attention items now survive a page reload and an archive instead of re-firing notifications or disappearing.
- Recycle and crash recovery, continued. Boot reconcile is guarded so one throw can't strand the whole fleet, and now protects every fleet-wide crash-recovery-eligible worktree at boot, not just the ones orphaned by that same boot's own crash; a manager-recycle handoff step is retried before the recycle is halted and the fleet is recovered if the successor then dies; a failed resume of a retired worker now leaves it retired, since the retirement is only lifted once the resumed spawn actually succeeds; and stopping a worker or using the kill switch no longer interferes with an in-flight merge gate.
- Gate scheduling is a little fairer and a little more correct. Queued gate waiters are woken when
maxConcurrentGatesis raised instead of waiting out their original poll; a merge confirm reuses an in-flight self-check instead of starting a redundant one; ownership is checked before attaching to an in-flight spawn or merge op; and a gate command with zero steps no longer counts as a pass. - Internal work. 75
test(...)commits hardened daemon fixtures, guards and Linux-CI parity (including an automatic advisory test run after every ungated landing, scoped to Loom's own repo); 20docs(...)commits extracted and corrected decision records; and 11perf(...)plus 6refactor(...)commits trimmed hot paths (bulk-loading boot-reconcile's per-session DB reads, making a couple of pty caches LRU, indexing an orchestration lookup) and removed dead code. Most of it changes tests, docs or internals with no user-visible behavior change.
Fixed
- The stored-credential env-var deny list closes more gaps, and a credential it silently de-provisions is now surfaced. The deny list used by a credential's env-var name (both at ask time and as a resolve-time backstop) now also blocks TLS/CA/proxy-bypass names (
SSL_CERT_FILE,SSL_CERT_DIR,REQUESTS_CA_BUNDLE,CURL_CA_BUNDLE,NODE_TLS_REJECT_UNAUTHORIZED,NODE_USE_ENV_PROXY, and theNPM_CONFIG_/PIP_prefixes) and a load-bearing-clobber class (SHELL,COMSPEC,SYSTEMROOT,TEMP/TMP/TMPDIR,APPDATA/LOCALAPPDATA). Widening the list can silently stop delivering an existing credential that was valid under the old rules —GET /api/projects/:id/delivered-credentialsnow marks such a rowdeliverable:false, reason:"reserved-name", and the daemon files one durable audit event per row (never repeats on a later spawn). If one of your own credentials is affected, revoke it and re-ask under a project-specific name (e.g.MYAPP_ANTHROPIC_KEYin place ofANTHROPIC_API_KEY). - The stored-credential env-var deny list now also covers the Codex harness's own auth/config vectors, several package managers, and a shell/interpreter-injection class. New prefixes:
CODEX_(the Codex CLI's ownCODEX_HOME/CODEX_API_KEY/CODEX_ACCESS_TOKEN/sandbox-override vectors — credentials are delivered to Codex spawns the same way they are to Claude),OPENAI_,YARN_,UV_,BUN_CONFIG_,OPENSSL_. New exact names:SSLKEYLOGFILE,BASH_ENV,ENV,ZDOTDIR,JAVA_TOOL_OPTIONS,_JAVA_OPTIONS,JDK_JAVA_OPTIONS,PERL5OPT,PERL5LIB,RUBYOPT,RUBYLIB,XDG_CONFIG_HOME,PATHEXT,WINDIR,CARGO_HTTP_CAINFO,BUNDLE_SSL_CA_CERT, andNODE_REPL_EXTERNAL_MODULE(the existingNODE_*names stay exact, not a prefix —NODE_AUTH_TOKENis a legitimate credential name aNODE_prefix would block). A small exact-name allow list is checked before each new prefix deny, for the one legitimate human-typed auth token each family has:YARN_NPM_AUTH_TOKEN,UV_PUBLISH_TOKEN,UV_PUBLISH_PASSWORD,BUN_CONFIG_TOKEN,CODEX_GITHUB_PERSONAL_ACCESS_TOKEN— a near-sibling redirect/config name in the same family (e.g.UV_INDEX_URL,BUN_CONFIG_REGISTRY,CODEX_HOME) still stays denied. As with every prior widening, a newly-denied name can de-provision an existing credential (deliverable:false, reason:"reserved-name") — revoke and re-ask under a project-specific name. TheOPENAI_prefix is a deliberate, non-free tradeoff, same asANTHROPIC_before it: it also blocks a legitimateOPENAI_API_KEYyou stored for your own app. If that's you, re-ask under a project-specific name (e.g.MYAPP_OPENAI_KEY). - Companion reliability, continued. A hung voice, media or chat-reply send now times out and reports as unconfirmed instead of hanging; inbound messages are now serialized per binding; a git commit warning now surfaces in the companion the same way it already did on the web; the heartbeat prompt and the proactive-home field now show their real effective value on the masked config instead of a stale or inferred one; and a manager reporting its queue has drained no longer pushes a repeat chat alert each time it re-reports a drained queue — the alert now fires once until the manager reports being busy again.
- Project and task correctness. A repo key that collides with a task worktree directory — including a case-only collision on a case-folding filesystem — is now rejected at creation; a vault-only project is now recorded as an explicit fact instead of inferred from git state; a
vaultPaththat aliases a code repo or another project's repo is refused on every create and update path, not just some; and a released task deferral now returns the persisted row and keeps its cleared-edge overlay even if the write that triggered it fails. - Webhook delivery dedupes correctly. Inbound GitHub and generic webhook deliveries are now deduped on a signed value, a cooldown-blocked replay now gets a
429through the durable nudge path, and a delivery's dedupe row is only undone when that specific delivery's fire had no effect. - Outbound connection fetches are bounded and diagnosable. Every connection fetch (SonarQube today, future connectors the same way) now goes through one helper that never follows a redirect, and a failed fetch's real underlying error code is now read correctly instead of being lost.
- Daemon stability. Secret-bearing fetches are bounded by timeout, size and redirect count; signal handlers and graceful shutdown are now registered before the boot sequence's own awaits, so an early boot failure can still shut down cleanly; a hung graceful shutdown is force-exited by a watchdog on a monotonic clock; and sessions are deleted atomically with every foreign-key child cascaded. Loom's own database layer now refuses to open the real
~/.loom/loom.dbfrom a test or ad-hoc script; only the daemon opens it by default. - The CLI is more careful about what it starts, stops and kills.
loom stopnow verifies a recorded pid's command line before hard-killing it; detached starts are serialized with an OS-level lock;LOOM_HOMEis carried into the Windows service task and systemd unit values are quoted; andloom updatenow stops a daemon with no PID file instead of hanging or failing silently. - Small MCP and skills fixes.
skill_editno longer corrupts a$-containing replacement pattern;question_resolveno longer races an owner's terminal reply against an agent's own turn; and a tilde (~) in a repo path now expands consistently across the REST, Setup and Platform create/update paths. - Minor reliability fixes. Codescape's supervised child process now has a bounded memory ceiling and its npm
cmd-shim parser is hardened against malformed input; the shared Python venv now shares one in-flight create/install across every consumer instead of racing duplicate installs; and a stale cached executable path now re-resolves instead of sticking forever.