Repository navigation
v2.21.0
[2.21.0] — 2026-09-30
Deprecated
-
monoswarmandautopilotare deprecated and will be removed in 2.22.0, the minor release after this one (#418). Both record state and start no agents. Use Claude Code's Task tool ormonomind org runinstead. Nothing is removed in this release:-
Every
monoswarmandautopilotsubcommand prints a one-line deprecation notice on stderr.-Qdrops it. The notice never goes to stdout, so the subcommands that emit JSON (--format json/--json) keep it clean. -
monomind startandmonomind start stoprecord and terminate a monoswarm state file, so they print the same notice.startno longer suggestsmonoswarm status, and its progress lines say it records state and starts no agents. When monoswarm is removed in 2.22.0,startstops recording monoswarm state andstart stopstops marking it terminated. -
The 13
monoswarm_*and 8autopilot_*MCP tools start their descriptions withDEPRECATED (removed in 2.22.0; use the Task tool or 'monomind org run'):and carry the full notice in adeprecatedfield on every result. The autopilot tool descriptions no longer claim to re-engage agents or report learning metrics. -
monoswarm statuscountsidleagent records as idle, not active. -
monoswarm init --format jsonprinted its progress lines and table on stdout ahead of the JSON. It now prints only the result document there. -
Honest wording:
monoswarm scalereports roster entries recorded or removed (it said "Spawned N agent(s)"),monoswarm stopsays it marks the state terminated (in its prompt, its output and the baremonoswarmhelp), andautopilot statusshowsTasks: 0/0 (n/a)with no tasks instead of100%.autopilot_statusandautopilot_progressreturnpercent: 0for zero tasks. -
The generated CLAUDE.md (full/security/performance templates),
.monomind/CAPABILITIES.md, the main--helpexamples, the guidance catalog and quick reference, and the shipped monoswarm skill, commands and agents that tell models to initialize a monoswarm now say it is deprecated and point to the Task tool ororg run. Section headings are unchanged, so upgrades of existing CLAUDE.md files are unaffected.
-
Removed
monomind.memory.learningBridge,memoryGraphandagentScopesconfig keys.initwrote them to.claude/settings.jsonand.monomind/config.yaml(withconfidenceDecayRate,accessBoostAmount,consolidationThresholdanddefaultScope),init upgradeadded them, and the wizard asked "Record agent memory scopes (project/local/user) in the config?", but no code, helper or hook read any of them.initandinit upgradeno longer write them, the wizard no longer asks, andCAPABILITIES.mddrops the "Memory Scopes (ADR-049)" section. Existing configs that carry the keys still load, andinit upgradekeeps them as they are.monomind.neural.enabledis unchanged: the SessionStart hook reads it.
Changed
-
The Claude Agent SDK and puppeteer's Chrome are no longer installed up front; they install on first use (#428).
npm install monomindpulled the ~300 MB native Claude binary through@anthropic-ai/claude-agent-sdk, and puppeteer's postinstall downloaded 686 MB of Chrome into~/.cache/puppeteer, for everyone. Measured from the packed tarballs on Linux x64, an install is now 263 packages and 938 MB ofnode_moduleswith nothing outside it, down from 344 packages, 1.23 GB and the Chrome download; four install scripts run instead of five. The SDK is now installed the first time a Claude org role,agent exec --runtime claudeoragent models --runtime claudeneeds it.monomind browsestill uses an installed Chrome, Chromium or Edge first; only when there is none does its first launch download Chrome for Testing through@puppeteer/browsers, anddesign detectof a URL uses that Chrome too. Both go into~/.monomind/deps($MONOMIND_HOME/deps), never into your project, with a notice on stderr. npm runsnpm cithere, in a staging directory, against lockfiles shipped with the CLI (every tarball must match their integrity hashes, whatever registry is configured), with--ignore-scriptsand one short fetch attempt, behind a lock, and the result is moved into place only once complete. Org roles cannot write the deps directory,~/.npmrcor~/.config/npm(file tools, the Claude sandbox, and a read-only bind in the bubblewrap mask), and monomind refuses to load a deps tree containing a symlink on the way in or out, a file owned by another user, or a group- or other-writable file. A copy of the SDK that monomind itself resolves is used only at the pinned version. Chrome is checked by HTTPS only; Chrome for Testing publishes no checksums.MONOMIND_NO_AUTO_INSTALL=1turns this off and prints the exact install command instead.@monoes/monodesignno longer lists puppeteer (its URL scans use monobrowse, and fall back to a puppeteer you install yourself), and monobrowse gainssetChromeFallback()and aMONOBROWSE_CHROME_PATHfallback that an installed browser always beats. The README anddoc/privacy.mddescribe what installs when. -
monomind initinstalls a small core pack; everything else is an opt-in pack (#411). Claude Code lists every skill and command with its description in about 1% of the context window (~8,000 chars on a 200k model) and drops descriptions past that. The default init installed 88 skills, 112 commands and 84 agents: ~39,500 chars of skill/command listing and ~13,000 of agent listing, so most descriptions never reached the model. It now installs the core pack: 26 skills (the mastermind engineering workflows, monolean, monodesign, monomotion, stop-slop, verification-quality, hooks, memory, GitHub and browser toolkits), 20 commands (/mastermind:do,review,plan,execute,debug,design,research,help, …) and 20 agents (the core dev agents plus Security Engineer, Frontend Developer, Software Architect, Technical Writer, Git Workflow Master, DevOps Automator, Monodesign and the GitHub PR/issue/release agents): 6,997 chars of skill/command listing and 3,094 of agent listing. The rest ships in eight opt-in packs:orgs,org-admin,swarm,github,testing,specialists(WeChat, Feishu, Solidity, embedded, SRE, …),businessandextras. Add them withmonomind init --packs orgs,github,--all-packs(or--full), the wizard's new pack question, or later withmonomind packs add <pack>;monomind packs listshows each pack and its listing size, andmonomind packs remove <pack>deletes only the pack's files that still match what init installed. Both keep the Kimi Code and OpenCode trees in step when a project has them.init --minimalnow installs the 26 core skills (still no commands or agents) instead of 2.20.0's 20-skill set.init skillstakes--packsin place of--memory/--github. Existing projects keep everything: re-runninginitorinit upgradedeletes nothing, andinit upgrade --add-missingnow adds only files for the packs a project has (a project from before packs counts every pack it has files for). -
Shipped skill, command and agent descriptions are capped at 200 characters (#411).
pnpm run lint:skillsfails on a longer one. The 62 over the cap were shortened, among them monodesign (1,080 chars), monolean (606), mastermind-liveness (434) and mastermind-monitor (426), and 15 more core ones were tightened.node scripts/measure-init-listing.mjsprints the listing size of a preset. -
monomind initsets up only the coding platforms installed on your machine (#420). It wrote Claude Code, Antigravity, OpenCode, Kimi Code and Codex into every project. It now checks each one's CLI onPATH(claude,gemini/agy/antigravity,opencode,kimi,codex; on Windows withPATHEXT) and its config directory (~/.claudeor$CLAUDE_CONFIG_DIR;~/.geminior~/.antigravity;~/.config/opencode,$XDG_CONFIG_HOME/opencodeor~/.opencode;~/.kimior~/.kimi-code;~/.codexor$CODEX_HOME), without running any of them, and writes the ones it finds plus the ones the project already has, soinit --forcestill refreshes and re-pins every platform's MCP entry. With none found it writes Claude Code and says so. Init prints what it detected and how to add the rest. On a machine with only Claude Code, a default init in an empty project now writes 162 files (2.4 MB) instead of 437 (5.3 MB), code graph included.--platforms a,bnames the platforms (--platformstill works, but not together with--platforms);--all-platformsand--target allwrite all five, and so does--fullalong with every component and pack.--all-platformswith a narrowing flag (--platforms,--target <one>,--only-claude,--skip-claude,--codex, …) is an error, and--skip-claudestill means all platforms but Claude Code. The wizard asks which platforms to set up before the component questions, with the detected ones pre-selected (all five for Full).init --jsonreportsplatforms: {source, selected, detected}, andinit skillscopies skills into.gemini/and.agents/only if the project has them.init upgrade --add-missingcopies new skills only into the.gemini/and.agents/trees a project already has, so an upgrade never adds a platform;init upgradedoes not refresh.codex/,opencode.json/.opencode/or.kimi-code/(re-runinit --yesfor that). The post-init doctor pass no longer installs the Claude Code CLI on its own: it asks first, only when Claude Code is selected and missing and someone is at a terminal, and otherwise printsnpm install -g @anthropic-ai/claude-code; aclaudein~/.local/binor~/.claude/localcounts as installed.init --jsonnever asks. Init still runs nonpm installin your project:package.jsonstays byte-identical and nonode_modulesappears. The code graph and the hooks use the@monoes/monographcopy bundled with the CLI (or a global install). -
The Claude runtime uses an installed Claude Code instead of downloading the SDK's copy (#522). Claude org roles,
agent exec --runtime claudeandagent models --runtime claudepass a usable installedclaudeto the SDK aspathToClaudeCodeExecutable, and the first-use install then fetches the SDK with--omit=optional(about 4 MB, still from the shipped lockfile) instead of 300 MB. monomind looks at$MONOMIND_CLAUDE_PATH, thenclaudeonPATH,~/.local/bin/claudeand~/.claude/local/claude, without running anything to find it. The real path runs, and it must be namedclaudeor the native installer'sversions/<x.y.z>and not be a script, so a shim that resolves to another program (mise) is never run. A binary found on its own is used only if it is a system install (not under$HOME, a temp dir or the cwd, and the file and every directory above it owned by root and not group- or other-writable), because org roles run as your user and could otherwise replace a binary the org daemon runs outside the sandbox; running as root, or on Windows, nothing is picked up on its own. Per-user installs (the native installer, mise, nvm, Homebrew) needMONOMIND_CLAUDE_PATH, which only has to be owned by you or root and not group- or other-writable (not checked on Windows). When it is not a system install, monomind warns and keeps it read-only for roles: denied to the file tools and in the Claude sandbox, and read-only in the bubblewrap mask with every directory between$HOMEand it pinned so none can be renamed aside.claude --version(once per process, no shell, 5-second timeout) must printx.y.z (Claude Code)with 2.x at or above the 2.1.226 the pinned SDK bundles. Anything else falls back to the bundled binary; a refusedMONOMIND_CLAUDE_PATHis reported on stderr, andMONOMIND_CLAUDE_PATH=bundledturns detection off. The SDK'squeryalways passes the chosen binary, and fails with a clear message if an update has removed it.doc/privacy.mddescribes the checks.
Fixed
-
org signandorg approve-pathsno longer list the live~/.claude/.config.jsonas a possible plant before monomind's first look (#548). With no first-look record for the home directory, the Claude Code global configs are not approval candidates, since the first look trusts every one present.org signprints them on a dim line instead: "will be trusted at monomind's first look: …". -
Codex home and cost follow-ups from the #537 review (#540). When
~/.codexor$CODEX_HOMEis a symlink (dotfile managers), the codex runner now resolves it and makes the real directory 0700 when the current user owns it, so its logs are no longer world-readable. It tightens each directory through anO_NOFOLLOW | O_DIRECTORYfile descriptor withfchmodinstead oflstat+chmod(the old calls remain the fallback where Node lacks those flags).org costskeeps a live $0 role cost as 0 instead of reporting it as unknown, and themastermind-budgetsskill marks the org spend as a lower bound when some roles report no cost. -
npm calls work on Windows again (#521). Node refuses to spawn
npm.cmdwithout a shell (EINVAL since 18.20.2 / 20.12.2), so the first-use installer, MCP auto-install,security cveand the update checker and installer failed on Windows. They now run npm'snpm-cli.jswith Node (found throughnpm_execpathor next tonode.exe), still without a shell. -
A
fileWrite/fileReadentry that names a directory now grants everything inside it (#492). Scope entries were always matched as globs, and a glob with no wildcard matches only that exact path, so a role withfileWrite: ["/srv/growth/site"]was denied every file insidesite, while the deny message told it that it "may use /srv/growth/site". An entry with no glob characters (*,?,[,{) now means that path and everything beneath it; a relative one resolves against the org workdir. It is compared on real paths, so a sibling with a shared prefix (site-old) and a symlink leading out of the directory are still denied, and the credential/guard deny list, the.gitwrite check and the decision-file check apply as before. Entries with glob characters match exactly as they did. A directory entry's real path is taken once, when the role starts, so a role cannot widen it by swapping the directory (or a not-yet-existing entry) for a symlink from Bash; an entry that no longer resolves to its startup path refuses the call. An entry that is already a symlink, or is/, a drive root,$HOMEor an ancestor of$HOME, grants nothing (write an explicit glob if that is really intended), and an empty-string entry is a config error. Deny messages now label each entry as a directory grant or a glob, and name absolute grants when a path is outside every root.org validate(after{{org_root}}/{{home}}expansion) reports a refused directory entry as an error, and warns when an absolute non-glob entry does not exist or a relative one leaves the role's workdir. Per-call approval results are also reworded: a pending or denied Bash/WebFetch/org_sendcall now says only that one call is waiting for or was refused by a human, that the task queue is not stuck, and what to do meanwhile (continue other work, don't retry the identical call while it is pending, pick another approach after a denial). The[approval] <action>: APPROVED|DENIEDinbox line keeps its prefix and adds the next step for the pending call(s). -
-o json/-o sarif/--jsonoutput is a clean document on stdout (#495).security scan -o sarifprinted its "Security Scan" banner and "Scan complete" line on stdout ahead of the document, somonomind security scan -o sarif > results.sarifwrote a file that SARIF uploaders reject;-o jsonhad the same problem, and-Qchanged neither.security scan,security cve --json,security defend -o jsonandperformance benchmark -o jsonnow send the banner, progress and summary lines to stderr, and-Qdrops them. Errors still go to stderr under-Q.security defenduses-Qfor--quick, so write--quietin full there. User-visible changes beyond JSON output:-
Spinner and progress-bar lines now go to stderr in every command, as
oradoes, socmd > fileno longer captures lines such as "Scan complete". This cleans the JSON output of commands that start a spinner before printing it, such ashooks coverage-gaps --format jsonandhooks worker run --format json. Under-Qthe spinner's progress line is dropped, but its completion line is kept. -
Commands whose only output is their result line still print it on stdout, and
-Qkeeps it:doc ingest,doc import,doc export,hooks intelligence export --output, andbrowse workflow run. -
security scan -Qin text mode drops the banner and the "Scan complete" line. It keeps the findings and the summary box.
-
-
monobrowse: two launches racing for the same default port no longer both fail when both lose the bind (#491). On Linux, two processes that call
listen()on the same port at the same instant can both getEADDRINUSE(each socket is marked LISTEN before the kernel's conflict check), leaving the port free; about 1 race in 600 locally. Both Chromes then exited early with nothing listening, whichlaunchBrowser()read as a Chrome that cannot start, so it failed instead of moving to the next candidate port ("Chrome exited before the CDP endpoint opened on port 23480 (code=21)" in CI). One such early exit per launch is now treated as a lost race and the scan moves on; a Chrome that really cannot start fails the same way on the next port, and the first failure is reported. -
A claude role's Bash tool uses the role's private TMPDIR (#503, follow-up to #480). Claude Code reads
CLAUDE_CODE_TMPDIRbeforeTMPDIR, and its Bash tool exports that variable to every command it runs. An org started from a Claude Code session's Bash tool inherited the outer session's value, so in the SDK sandbox a claude role's Bash got that shared directory as$TMPDIRand a baremktemplanded there:$HOME/mrg-tmp/claude-1000/claude-1000in the 2.20.0 release run's drill. Without the sandbox,$TMPDIRwas right, but aclaudestarted from the role's shell still used the shared directory. The role environment now setsCLAUDE_CODE_TMPDIRto the role's directory as well asTMPDIR,TMPandTEMP. Claude Code keeps its own files inclaude-<uid>/inside that directory. The sandbox is unchanged: its writable roots already include the base the role directory sits in. A new opt-in test (MONOMIND_SANDBOX_E2E=1) runs the bundled Claude Code CLI against a scripted API, both unsandboxed and in the sandbox, and checks$TMPDIR,mktemp -dand$CLAUDE_CODE_TMPDIR. -
The pre-bash hook no longer prints git's "not a git repository" error in a project without git (#506). A
grep/findcommand reached the monograph lookup, which runsgit rev-parseto find the repo root with git's stderr passed through, so a project outside a git repository (or across a filesystem boundary) gotfatal: not a git repository …on the hook's stderr. The helper's git calls inutils/monograph.cjsnow discard git's stderr; the result was already ignored on failure.tests/hooks/security-gates.test.mjsalso stops inheritingMONOMIND_HOOK_QUIETfrom a live session (it skipped the code path the fail-open test crashes) and setsGIT_CEILING_DIRECTORIES, so it passes the same on tmpfs and disk-backedTMPDIR. -
Security: org roles can no longer rewrite their own org definition, decisions or runtime state with file tools (#498). The file tools refused only the four decision files (
gates.json,approvals.json,questions.json,inbox.jsonl). A role whosefileWritereached the org root could Write or Edit.monomind/orgs/<org>.json, which holds its ownpolicy(scopes, git level,sandbox.allowWrite), and have that apply on reload or at the next run. It could also rewritedecisions.jsonl. This includes the defaultfileWrite: ["**"]when the workspace is the project root. On the Claude runtime,Write,Edit,MultiEditandNotebookEditare now refused, regardless of scope, root orallowWrite, on these files under the org root's.monomind/orgs/:-
every file directly in it (org definitions and their
-state,-secrets,-runstate, … files); -
every file directly in an org dir (decision files,
runtime.json,decisions.jsonl,history.jsonl,idle-watchdog.json, and therun/stop/pause/reloadcontrol files); -
a run dir's
bus.jsonlandsessions.json; -
anything under
git-guard/.
Every path argument of a call is checked. Paths are classified both as written and after resolving symlinks (dangling ones included), and hard links are compared by inode. On macOS and Windows, segments are compared case-insensitively, and on Windows without trailing dots, spaces or:streamsuffixes. The.gitwrite check now compares the same way.reports/,work/,workspace/,worktree/,.mail/and<x>-memory/stay writable, and so does a checkout's own.monomind/orgs/.
For Bash, and for the native file tools of other CLI runtimes (codexapply_patch, kimi, …), which never reach that check: -
The bubblewrap authority mask binds the orgs dir read-only and binds only the role work dirs back read-write. Each org's
work,reports,runs,scratch,workspaceand.mail, and any<dir>a role'sfileWritenames, are created first, sogit worktree add … work/srcstill works. The org root and.monomindcan't be renamed. A planted symlink (ln -s .. orgs/foo,ln -s $HOME orgs/foo) can't loosen the mask or uncover the hidden credentials. -
The SDK sandbox makes the existing authority files read-only, including every org's whole
git-guard/. The orgs tree and every dir in an org dir can't be renamed or swapped. Bash there can still create a new org definition plus itsrunfile, whichorg servewould start, or pre-create an authority file the daemon hasn't written yet.org-runtime.md("Authority files") lists these and the other limits.
-
-
monomind startuses the topology and maxAgents from.monomind/config.yaml(#509).initwrites that section asswarm:, butstartreadmonoswarm:, so it always fell back tohierarchical-meshand 15 agents. It now readsswarm:and still accepts amonoswarm:section. The--topologyflag still takes precedence. -
The guidance catalog named skills that do not exist.
guidance_capabilitiesandguidance_recommendlistedmemory-advanced,memory-vector-search,memory-patterns,memory-learning,memory-optimizationand fivegithub-*skills; none ship. The memory and GitHub areas now point to the shippedmemory-toolkitandgithub-toolkit; the embeddings area links no skill, since none covers theembeddings_*tools. A test fails if the catalog names a skillmonomind initdoes not install. -
Security: org file-tool path checks fold case and Unicode look-alikes; the Bash git check matches
gitin any case (#496). On a case-insensitive filesystem (macOS, Windows, vfat/exfat, a Linux casefold dir) a role could reach a denied path through another spelling of it. Deny checks now fold every path segment on every platform, whatever the filesystem: NFKC normalization, removal of default-ignorable code points (zero-width space and joiners, soft hyphen, BOM, CGJ, which Linux casefold ignores), and full case folding (so.SSH,.ſsh,.ßh,.ẞh,.gıt,.gi<ZWJ>t,.ss<soft hyphen>h,.config,.profile,daſhboard-tokenand a Kelvin-signKall match), with trailing dots and spaces and a:streamsuffix dropped. This covers the credential and guard deny list (~/.GITCONFIG,~/.BashRC), the dashboard token, the org authority files and the.gitwrite check. On a case-sensitive filesystem a deny now also refuses look-alike names, for example aDashboard-Tokenfile anywhere, or anything under.monomind/Orgs/of the org root, on Linux. That fail-closed trade-off is intended: the policy cannot always tell whether a directory folds case, and a missed deny is worse than a refused look-alike. Grants (roots and directory entries infileWrite/fileRead) compare the on-disk spelling thatrealpathreturns and fold only the part of a path that does not exist yet. They do that only on macOS and Windows, and only where a probe of the target directory (a case-swapped lookup of its own name and of one of its entries, by inode and never through a symlink) shows that the filesystem folds case. Globs never fold, and a grant never reaches an existing directory spelled in another case. A directory entry spelled in another case than the directory on disk is no longer refused as a symlink. The Bash git classifier matchesGIT,/usr/bin/GIT,git.EXE,BASH -c "git …",ENV -i …,cmd /c,powershell/pwshand other interpreters in any case, so at levelreadGIT pushis refused likegit push. -
init --only-claudewith another platform flag is an error, and--skip-claudeno longer suggests adding Claude Code back (#525).init --only-claude --codexquietly wrote Claude Code only;--only-claudewith--platforms/--platformnaming another platform,--target <other>,--codex/--opencode/--kimicodeor--skip-claudenow fails with--only-claude cannot be combined with …(--target claudeand--platforms claudeare still accepted). Afterinit --skip-claude, the "Not written" hint left Claude Code in its--platformslist and offered--all-platforms; it now names only the other platforms and its suggested command keeps--skip-claude. -
--runtime hermesworks with hermes 0.19.0 (#531). The runner ranhermes chat --query-file <file> --oneshot -Q, and hermes 0.19.0 (the PyPI release) has neither flag, so every turn failed withunrecognized arguments. The runner now readshermes chat --help(stdout and stderr) and caches the answer once a probe exits cleanly; a timed-out or failed probe is retried on the next run, and the probe stops when the run is aborted. A build that lists--query-filekeeps that form, and otherwise the runner runshermes chat --query=<prompt> -Q, which in 0.19.0 answers one query and exits. With 0.19.0 the prompt therefore travels on the command line: other local users can read it inpsor/procwhile the turn runs, and a prompt over 128 KiB fails with a clear error instead ofE2BIG. A failed turn with nothing on stderr now shows what hermes printed on stdout, where 0.19.0 puts its "isn't configured yet" setup error; that line maps toauth, and the rest of stdout is shown but never used to pick the error code. -
--runtime pi-rpcreports a missing API key at once instead of timing out (#532). Without a key, pi 0.87.1 in--mode rpcanswers the prompt withsuccess:false("No API key found for openai") and sends nothing else. The runner ignored that reply and waited on its 10-minute silence watchdog. It now fails the turn withmissing API key: set OPENAI_API_KEY for provider openai, or run \pi` then /login, and pi's own message. When a model is set, the runner first asks pi withpi auth check --model --json(about 0.3 s): onlynot_readystops the run before pi starts, as a missing key or, for other reasons, "not logged in or the login expired". pi resolves the provider itself, so an OpenRouter-routed id such asdeepseek/deepseek-chat-v3.1with onlyOPENROUTER_API_KEYset runs normally. A pi withoutauth check`, or a check that fails, skips the pre-check. Any other rejected prompt also fails the turn, and pi's no-key text on stderr when it exits is reported the same way.agent execreports a missing API key aserror {code:"auth", fatal:true}on every runtime (Agent Exec Protocol rev 27). A message saying "missing API key", "no API key found/configured", or hermes's "no inference provider configured" / "no API keys or providers found" wasrunner-error; §3.4'sauthnow also covers a key that was never set. Text a runner attaches but did not write itself (hermes's stdout, cline's final model text) follows a[output below is not classified]line and never decides the error code, so a model saying "401" can no longer turn a failure intoauth.
-
An unknown cost is reported as
null, not0(#533). A runtime that reports no cost (codex, kimicode, hermes, vercel and the otherreports_cost: falseruntimes) showedcost_usd: 0inagent exec'susageandresultevents, inagent test --jsonwhen no tokens were reported, and in orgusageevents,org reportandorg costs— the same value as a free turn. Those fields are nownullwhen no cost was reported, and totals are the sum of the reported costs ornullwhen there are none. When only some usage had a cost, the total is a lower bound andorg report --json/org costs --jsonaddcost_complete: false. Anullcost never counts toward--budget-usdor a role'sbudget_usd. The human-readableorg costs,org report --by-role,org logs --stats, run-end line andorg statussayunknown; the dashboard's run history saysunpriced. vercel and hermes no longer yield a fakecost_usd: 0. Protocol rev 28, capabilityagent-exec-cost-null. -
agent modelsmarks aliases of the same model (#534). Claude'sdefaultandopusaliases both resolve to the same model, so a caller that tested every listed model ran and paid for it twice. Every entry is still listed, so a lookup by id such asopuskeeps working; an entry that resolves to the same model as an earlier one now carriesalias_ofwith that entry's id ("alias_of":"default"onopus), and the earlier entry lists all of them inaliases. Skipalias_ofentries to test each model once. The table view shows(alias of default). What each alias resolves to is unchanged. Capabilityagent-models-alias-of. -
MonoFence no longer installs monofence-ai into your project (#519). When monofence-ai was missing, the monofence_* MCP tools ran
npm install monofence-ai --no-savein the current directory, which created or changednode_modulesin the user's project, and then asked for an MCP server restart. They now use the first-use installer, which puts monofence-ai@1.0.7 (pinned, with a shipped lockfile,--ignore-scripts) into~/.monomind/depsand loads it from there without a restart.security defend,security redteamand org role fences load it the same way; they imported it by name, which never saw that directory. A failed install reports the installer's own message, including the command to install by hand, and is not retried in the same session. The installer now also resolves ESM-only packages such as monofence-ai, whoseexportshave only animportcondition; before, it could not load one even once installed. -
design detect <url>anddesign fix <path>use their positional target (#520). The--targetoption had a parser default of., which always won over the positional argument, somonomind design detect https://example.comscanned the current directory instead (anddesign fix ./srcfixed the current directory). The positional target is now used when-t/--targetis not given;-tstill works, and with neither the target is still..
Security
-
codex no longer writes your environment to a world-readable shell snapshot (#535). codex's
shell_snapshotfeature is on by default in codex 0.156.1, andcodex exec, which monomind's codex runner uses, creates$CODEX_HOME/shell_snapshots/. A snapshot holds the user's shell environment, including every exported API key and token, and is created with the process umask, so 0644 (world-readable) under the usual umask 022. The runner now passes-c features.shell_snapshot=false -c features.shell_snapshot_v2=falseon everycodex exec, so no snapshot is written. Before each spawn it also makes$CODEX_HOME(default~/.codex) and itssessions/andshell_snapshots/0700 when you own them, which hides the files inside from other users whatever their own modes. The process umask is not changed, so files the agent writes in the workspace keep their usual modes. Snapshots already on disk (from monomind runs or interactive codex) stay there: delete~/.codex/shell_snapshots/*.shand rotate the keys they contain. -
Org definitions take effect only when the operator has signed them (#502). A role whose Bash could write
.monomind/orgs/(an unsandboxed or full-access role, a runtime whose own file tools are not policy-gated, or the SDK sandbox, which lets Bash create new files there) could addevil.jsonwith apolicy.git: pushrole plus anevil/runfile, andorg servestarted it with push credentials. A role could also widen its own org'spolicyfor the next reload or run.access_ackcovered only full-access grants. Now:-
monomind org sign <org>(and--all) prints everything that decides what the roles may run, reach or be granted (tool provider commands and env names, adapter and provider config, sandboxallowWrite/allowedDomains, skills,instructions_file, precheck commands, federation, fence), names every role that would run unconfined on this machine, shows a diff against the last signed version, and signs it. The signature covers every field of the definition except the goal and each role'stitle,responsibilitiesandui, so each role's wholepolicy, the roles with their runtimes and adapters,instructions_file,skills/skill_pool,run_config(includingprechecks),schedule,runtime,fence,federationandloadoutsare covered; a__proto__/constructor/prototypekey is refused. It is an HMAC under the same machine-local key as full-access grants, stored beside that key in~/.monomind/orgrt-operator/, which no role can read or write. The key and signature files must be the operator's own, mode 0600 and not symlinks, and a key replaced after a daemon loaded it is refused. It is not stored in the org file, so tracked org files stay unchanged.signrefuses inside an org role's oragent exec's process tree; without a TTY and--yesit prints the review and signs nothing. -
Enforced where a definition takes effect:
org run,org serve's runfile poll and schedule (before a scheduled org'sprechecksrun), resume from a checkpoint, andorg reload. An unsigned or changed definition is refused with "runmonomind org sign <org>as the operator after reviewing the change". A refused reload changes nothing: the running org keeps its last verified definition and emits ahot-reload-refusedaudit event. Goal and prompt edits need no new signature. -
Migration: existing orgs are unsigned.
org runon a TTY offers a one-time review and sign;org serve, a detachedorg runand every other non-TTY path refuse until the operator runsmonomind org sign <org>, ormonomind org sign --allonce per checkout (this includes the shippedrelease,monomind-devandsample-teamorgs).org createand/mastermind:createorgsign the org they write.org role set-accessre-signs an org that was signed before its edit. -
Roles can no longer write what the operator's own sessions run or trust: the org root's
.claude/and.mcp.json,~/.claude.json(read-only in the bubblewrap mask), both org skill libraries,~/.monomind(except the CLI's own state such asprojects/, browser state andmodels/), the terminal-execution gate,~/.npm/_npx,~/.npmrc,~/.local/binand more shell startup dirs. They are refused to the file tools, in the SDK sandbox'sdenyWriteand read-only in the bubblewrap mask; the skill libraries and the terminal gate (as disabled) are created before a role starts. A signedpolicy.sandbox.allowWriteentry opens one of them. A role can no longer fill npx's cache; pointnpm_config_cacheat$TMPDIRif it must. In the mask~/.monominditself is read-only with only those entries bound back, and cline and aider keep an org role's state in its$TMPDIR.mcp stopsignals the pid in~/.monomind/mcp.pidonly when its command line is a monomind MCP server, and the terminal-execution gate is read from$MONOMIND_HOMEwhen set, where it is protected.doctor -c org-skillsandorg signlist org skills that come from the project or user library. -
instructions_fileis read only from inside the project, through every symlink, and never from the operator or dashboard-auth directories, a credential store or a dashboard token. Its content is signed as a digest; the file is read through a descriptor opened withO_NOFOLLOWand checked to be the verified inode with no other hard link, and a session reads it only while its content matches the digest verified at org start. -
A path a role plants because it did not exist yet is quarantined:
~/.claude/.config.json(which Claude Code prefers over~/.claude.json) and other.claude*.jsonvariants that were not there at monomind's first look (recorded once; a live legacy config the operator already had is left alone), a.mcp.jsonin an org work tree that is untracked or differs from the main checkout's, and every other protected path that was missing when a role session started. That baseline is kept in the operator dir, so a crashed or killedorg rundoes not make a plant look pre-existing, and onlymonomind org approve-paths <path>…(a new subcommand, TTY only, never from a role) trusts a path; signing approves nothing. Checks run at every org start, every session start and end, every 2-secondorg runandorg servetick, and everydoctorrun. Anything found is moved (never deleted) to~/.monomind/orgrt-operator/quarantine/<project>/<ts>/, out of every role's reach, with an audit event and an operator question giving the exactmv … && monomind org approve-paths …to restore it. The first look prints what it trusted.doctor -c org-skillswarns when.mcp.jsonservers are approved by name but.mcp.jsonis missing, untracked or modified. -
org test-loopruns its fixture orgs in a throwaway directory, not in the project, and removes their signatures. -
Residual: a role that can read the operator-credential directory can sign, as it can already forge a full-access grant: an active full-access role, or a role that runs with neither the SDK sandbox nor the bubblewrap mask. Such roles are allowed, by decision: refusing them would stop orgs that need one (a push-level releaser, codex on macOS, any role on a host without bubblewrap), so signing protects every other role and these run with a warning instead.
org signand every org start name each unconfined role and why it runs without an OS sandbox (for examplepolicy.git push, and no bubblewrap mask). To confine one, run it on the claude runtime belowpolicy.git: pushwith the sandbox on, or install bubblewrap (Linux).
-
-
Org roles can no longer overwrite the operator's node, npm, claude or monomind installed under
$HOME, or plant a file on the operator'sPATH(#527). With node from mise, nvm, volta, fnm, asdf or bun, the SDK sandbox'sallowWrite(which includes$HOME) and the bubblewrap mask both let a sandboxed role replacenode,npmor monomind's own code, or drop agitinto a writablePATHdirectory, which the daemon and the operator's shells then ran outside every sandbox. At every org and role-session start monomind now resolves the real paths ofprocess.execPath, the running CLI's package root, every writable directory onPATH(and the directory holding a symlink on the way to one, such as fnm's multishells), andnode/npm/npx/claudeonPATH, each with its install root, plus the version-manager roots (mise, nvm, volta, fnm, asdf, rustup, pyenv, rbenv,~/.cargo/bin,~/go/bin, bun's global installs, pnpm's home as a whole), honouring$XDG_DATA_HOME/$XDG_STATE_HOME/$XDG_CONFIG_HOMEand each tool's own variable. Also covered: mise's global config dir and trust store,~/.mise.toml,~/.tool-versions, direnv's allow list and config dir (a role could otherwise write or trust a config that every mise- or direnv-activated operator shell then runs), and toolchain config files (~/.bunfig.toml,~/.cargo/env,~/.cargo/config.toml,~/.yarnrc.yml,~/.default-npm-packages, pnpm'src, Go'senv). The list joins the operator-protected paths, so it is refused to the file tools, in the SDK sandbox'sdenyWrite, read-only in the mask and watched for plants; the install dirs a tool in use expects are created empty up front, so the operator's ownpnpm add -gis never taken for a plant. Every directory above a protected path (~/.local,~/.local/share, …) is now a mount point in both OS layers, so a role cannot rename it aside and plant a new tree; this also closes that route for~/.local/binand~/.npm/_npx. An org root at$HOMEno longer uncovers~/.monomindin the mask, and excludes noPATHdirectory (with a warning). Roles still run node, npm and claude. A role that installs a global tool getsEROFS;npm_config_prefix=$TMPDIR/npm-globalinstalls it in the role's temp dir. A role's pnpm store moves to$XDG_DATA_HOME/pnpm-store(npm_config_store_dir) and pnpm no longer self-installs versions in a role (npm_config_manage_package_manager_versions=false); bun's cache stays writable. -
First-use code is pinned by hash, and roles can no longer plant missing config or move a custom
MONOMIND_HOMEaside (#526, follow-ups to the #518 review).-
Pinned hashes. The Claude Agent SDK's
sdk.mjs, each platform's Claude binary, and everydist/*.jsfile of monofence-ai (which has no dependencies) have SHA-256 hashes shipped next to the install lockfiles, taken from the registry tarballs (the procedure is documented beside them). They are hashed once per process before the package is imported, whether the copy is in~/.monomind/depsor found up monomind's own module path. Everyquery()then passes the verified bundled binary aspathToClaudeCodeExecutable, so the SDK never picks its own (an installed Claude Code that #522 selected is passed as is and not pin-checked), and onestatbefore the query and again before a spawn hook refuses a binary that changed since it was hashed. The first Claude session in a process waits for the hash of the about 300 MB binary (well under a second on a current machine).sdk.mjsis imported by path right after its hash, a window of milliseconds that only an unsandboxed process could use (roles cannot write the deps dir).@puppeteer/browsersis not pinned (it loads a whole dependency tree); a test fails for any optional dependency that is neither pinned nor listed as unpinned with a reason. A same-user plant passed the ownership and mode checks; now a mismatch refuses to load, names the file and says what to delete. A test keeps the pins in step with the pinned version and lockfile, and checks them against the copy the checkout installed. -
Missing config is created before a role starts. The SDK sandbox drops a
denyWritepath that does not exist, so a role could create~/.npmrc,~/.config/npmor a shell rc file. At org and session start, in the operator's HOME and only where the path is absent (nothing is ever overwritten), monomind now creates:~/.npmrc,~/.bashrc,~/.profileas empty files (mode 0600). Empty, each runs or sets nothing: npm merges nothing from it, interactive bash sources nothing, and~/.profileis read by sh/dash login shells, and by bash only without.bash_profile/.bash_login, never by zsh.~/.profileis not created when$SHELLis zsh or fish: installers such as nvm's append to~/.profilewhen it exists instead of that shell's rc file;~/.ssh,~/.config/git,~/.config/gh,~/.config/npmas empty mode-0700 directories (and~/.configif missing). Tools read files inside them, not the directories.
Linux only. On macOS only
~/.configis created: seatbelt denies a missing path by rule, so the missing entries are passed to it instead (see below). The SDK sandbox then denies them and the mask binds them read-only. Left to the planted-path watch, because an empty one could change behaviour:~/.bash_profileand~/.bash_login(bash would skip~/.profile),~/.gitconfig(git config --globalwould stop writing a later~/.config/git/config) and the zsh startup files (zsh's new-user setup would not run). -
No rename aside through an ancestor. Every directory above
$MONOMIND_HOMEand~/.monomindwhose parent a role can write is a mount point in the SDK sandbox and the bubblewrap mask. monomind refuses to load from a deps root reached through a symlink in a directory the user can write. -
macOS. The SDK's seatbelt profile already denies unlinking and creating every ancestor of a denied path, so the rename-aside defence holds there without mount points. Seatbelt rules match paths, so on macOS a
denyWriteentry that does not exist yet (with an existing parent) is now passed too, and creating it is denied.
-
npm: monomind@2.21.0, @monoes/monomindcli@2.21.0, @monoes/monobrowse@1.0.30, @monoes/monodesign@1.2.24, @monoes/hooks@1.0.13