Skip to content

v2.21.0

Choose a tag to compare

@nokhodian nokhodian released this 30 Sep 14:26
· 406 commits to main since this release

[2.21.0] — 2026-09-30

Deprecated

  • monoswarm and autopilot are 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 or monomind org run instead. Nothing is removed in this release:

    • Every monoswarm and autopilot subcommand prints a one-line deprecation notice on stderr. -Q drops it. The notice never goes to stdout, so the subcommands that emit JSON (--format json / --json) keep it clean.

    • monomind start and monomind start stop record and terminate a monoswarm state file, so they print the same notice. start no longer suggests monoswarm status, and its progress lines say it records state and starts no agents. When monoswarm is removed in 2.22.0, start stops recording monoswarm state and start stop stops marking it terminated.

    • The 13 monoswarm_* and 8 autopilot_* MCP tools start their descriptions with DEPRECATED (removed in 2.22.0; use the Task tool or 'monomind org run'): and carry the full notice in a deprecated field on every result. The autopilot tool descriptions no longer claim to re-engage agents or report learning metrics.

    • monoswarm status counts idle agent records as idle, not active.

    • monoswarm init --format json printed its progress lines and table on stdout ahead of the JSON. It now prints only the result document there.

    • Honest wording: monoswarm scale reports roster entries recorded or removed (it said "Spawned N agent(s)"), monoswarm stop says it marks the state terminated (in its prompt, its output and the bare monoswarm help), and autopilot status shows Tasks: 0/0 (n/a) with no tasks instead of 100%. autopilot_status and autopilot_progress return percent: 0 for zero tasks.

    • The generated CLAUDE.md (full/security/performance templates), .monomind/CAPABILITIES.md, the main --help examples, 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 or org run. Section headings are unchanged, so upgrades of existing CLAUDE.md files are unaffected.

Removed

  • monomind.memory.learningBridge, memoryGraph and agentScopes config keys. init wrote them to .claude/settings.json and .monomind/config.yaml (with confidenceDecayRate, accessBoostAmount, consolidationThreshold and defaultScope), init upgrade added 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. init and init upgrade no longer write them, the wizard no longer asks, and CAPABILITIES.md drops the "Memory Scopes (ADR-049)" section. Existing configs that carry the keys still load, and init upgrade keeps them as they are. monomind.neural.enabled is 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 monomind pulled 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 of node_modules with 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 claude or agent models --runtime claude needs it. monomind browse still uses an installed Chrome, Chromium or Edge first; only when there is none does its first launch download Chrome for Testing through @puppeteer/browsers, and design detect of a URL uses that Chrome too. Both go into ~/.monomind/deps ($MONOMIND_HOME/deps), never into your project, with a notice on stderr. npm runs npm ci there, in a staging directory, against lockfiles shipped with the CLI (every tarball must match their integrity hashes, whatever registry is configured), with --ignore-scripts and one short fetch attempt, behind a lock, and the result is moved into place only once complete. Org roles cannot write the deps directory, ~/.npmrc or ~/.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=1 turns this off and prints the exact install command instead. @monoes/monodesign no longer lists puppeteer (its URL scans use monobrowse, and fall back to a puppeteer you install yourself), and monobrowse gains setChromeFallback() and a MONOBROWSE_CHROME_PATH fallback that an installed browser always beats. The README and doc/privacy.md describe what installs when.

  • monomind init installs 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, …), business and extras. Add them with monomind init --packs orgs,github, --all-packs (or --full), the wizard's new pack question, or later with monomind packs add <pack>; monomind packs list shows each pack and its listing size, and monomind 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 --minimal now installs the 26 core skills (still no commands or agents) instead of 2.20.0's 20-skill set. init skills takes --packs in place of --memory/--github. Existing projects keep everything: re-running init or init upgrade deletes nothing, and init upgrade --add-missing now 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:skills fails 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.mjs prints the listing size of a preset.

  • monomind init sets 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 on PATH (claude, gemini/agy/antigravity, opencode, kimi, codex; on Windows with PATHEXT) and its config directory (~/.claude or $CLAUDE_CONFIG_DIR; ~/.gemini or ~/.antigravity; ~/.config/opencode, $XDG_CONFIG_HOME/opencode or ~/.opencode; ~/.kimi or ~/.kimi-code; ~/.codex or $CODEX_HOME), without running any of them, and writes the ones it finds plus the ones the project already has, so init --force still 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,b names the platforms (--platform still works, but not together with --platforms); --all-platforms and --target all write all five, and so does --full along with every component and pack. --all-platforms with a narrowing flag (--platforms, --target <one>, --only-claude, --skip-claude, --codex, …) is an error, and --skip-claude still 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 --json reports platforms: {source, selected, detected}, and init skills copies skills into .gemini/ and .agents/ only if the project has them. init upgrade --add-missing copies new skills only into the .gemini/ and .agents/ trees a project already has, so an upgrade never adds a platform; init upgrade does not refresh .codex/, opencode.json/.opencode/ or .kimi-code/ (re-run init --yes for 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 prints npm install -g @anthropic-ai/claude-code; a claude in ~/.local/bin or ~/.claude/local counts as installed. init --json never asks. Init still runs no npm install in your project: package.json stays byte-identical and no node_modules appears. The code graph and the hooks use the @monoes/monograph copy 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 claude and agent models --runtime claude pass a usable installed claude to the SDK as pathToClaudeCodeExecutable, 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, then claude on PATH, ~/.local/bin/claude and ~/.claude/local/claude, without running anything to find it. The real path runs, and it must be named claude or the native installer's versions/<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) need MONOMIND_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 $HOME and it pinned so none can be renamed aside. claude --version (once per process, no shell, 5-second timeout) must print x.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 refused MONOMIND_CLAUDE_PATH is reported on stderr, and MONOMIND_CLAUDE_PATH=bundled turns detection off. The SDK's query always passes the chosen binary, and fails with a clear message if an update has removed it. doc/privacy.md describes the checks.

Fixed

  • org sign and org approve-paths no longer list the live ~/.claude/.config.json as 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 sign prints 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 ~/.codex or $CODEX_HOME is 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 an O_NOFOLLOW | O_DIRECTORY file descriptor with fchmod instead of lstat + chmod (the old calls remain the fallback where Node lacks those flags). org costs keeps a live $0 role cost as 0 instead of reporting it as unknown, and the mastermind-budgets skill 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.cmd without a shell (EINVAL since 18.20.2 / 20.12.2), so the first-use installer, MCP auto-install, security cve and the update checker and installer failed on Windows. They now run npm's npm-cli.js with Node (found through npm_execpath or next to node.exe), still without a shell.

  • A fileWrite/fileRead entry 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 with fileWrite: ["/srv/growth/site"] was denied every file inside site, 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 .git write 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, $HOME or 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_send call 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|DENIED inbox line keeps its prefix and adds the next step for the pending call(s).

  • -o json/-o sarif/--json output is a clean document on stdout (#495). security scan -o sarif printed its "Security Scan" banner and "Scan complete" line on stdout ahead of the document, so monomind security scan -o sarif > results.sarif wrote a file that SARIF uploaders reject; -o json had the same problem, and -Q changed neither. security scan, security cve --json, security defend -o json and performance benchmark -o json now send the banner, progress and summary lines to stderr, and -Q drops them. Errors still go to stderr under -Q. security defend uses -Q for --quick, so write --quiet in full there. User-visible changes beyond JSON output:

    • Spinner and progress-bar lines now go to stderr in every command, as ora does, so cmd > file no longer captures lines such as "Scan complete". This cleans the JSON output of commands that start a spinner before printing it, such as hooks coverage-gaps --format json and hooks worker run --format json. Under -Q the 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 -Q keeps it: doc ingest, doc import, doc export, hooks intelligence export --output, and browse workflow run.

    • security scan -Q in 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 get EADDRINUSE (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, which launchBrowser() 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_TMPDIR before TMPDIR, 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 $TMPDIR and a bare mktemp landed there: $HOME/mrg-tmp/claude-1000/claude-1000 in the 2.20.0 release run's drill. Without the sandbox, $TMPDIR was right, but a claude started from the role's shell still used the shared directory. The role environment now sets CLAUDE_CODE_TMPDIR to the role's directory as well as TMPDIR, TMP and TEMP. Claude Code keeps its own files in claude-<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 -d and $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/find command reached the monograph lookup, which runs git rev-parse to find the repo root with git's stderr passed through, so a project outside a git repository (or across a filesystem boundary) got fatal: not a git repository … on the hook's stderr. The helper's git calls in utils/monograph.cjs now discard git's stderr; the result was already ignored on failure. tests/hooks/security-gates.test.mjs also stops inheriting MONOMIND_HOOK_QUIET from a live session (it skipped the code path the fail-open test crashes) and sets GIT_CEILING_DIRECTORIES, so it passes the same on tmpfs and disk-backed TMPDIR.

  • 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 whose fileWrite reached the org root could Write or Edit .monomind/orgs/<org>.json, which holds its own policy (scopes, git level, sandbox.allowWrite), and have that apply on reload or at the next run. It could also rewrite decisions.jsonl. This includes the default fileWrite: ["**"] when the workspace is the project root. On the Claude runtime, Write, Edit, MultiEdit and NotebookEdit are now refused, regardless of scope, root or allowWrite, 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 the run/stop/pause/reload control files);

    • a run dir's bus.jsonl and sessions.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 :stream suffixes. The .git write 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 (codex apply_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, workspace and .mail, and any <dir> a role's fileWrite names, are created first, so git worktree add … work/src still works. The org root and .monomind can'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 its run file, which org serve would 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 start uses the topology and maxAgents from .monomind/config.yaml (#509). init writes that section as swarm:, but start read monoswarm:, so it always fell back to hierarchical-mesh and 15 agents. It now reads swarm: and still accepts a monoswarm: section. The --topology flag still takes precedence.

  • The guidance catalog named skills that do not exist. guidance_capabilities and guidance_recommend listed memory-advanced, memory-vector-search, memory-patterns, memory-learning, memory-optimization and five github-* skills; none ship. The memory and GitHub areas now point to the shipped memory-toolkit and github-toolkit; the embeddings area links no skill, since none covers the embeddings_* tools. A test fails if the catalog names a skill monomind init does not install.

  • Security: org file-tool path checks fold case and Unicode look-alikes; the Bash git check matches git in 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-token and a Kelvin-sign K all match), with trailing dots and spaces and a :stream suffix dropped. This covers the credential and guard deny list (~/.GITCONFIG, ~/.BashRC), the dashboard token, the org authority files and the .git write check. On a case-sensitive filesystem a deny now also refuses look-alike names, for example a Dashboard-Token file 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 in fileWrite/fileRead) compare the on-disk spelling that realpath returns 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 matches GIT, /usr/bin/GIT, git.EXE, BASH -c "git …", ENV -i …, cmd /c, powershell/pwsh and other interpreters in any case, so at level read GIT push is refused like git push.

  • init --only-claude with another platform flag is an error, and --skip-claude no longer suggests adding Claude Code back (#525). init --only-claude --codex quietly wrote Claude Code only; --only-claude with --platforms/--platform naming another platform, --target <other>, --codex/--opencode/--kimicode or --skip-claude now fails with --only-claude cannot be combined with … (--target claude and --platforms claude are still accepted). After init --skip-claude, the "Not written" hint left Claude Code in its --platforms list and offered --all-platforms; it now names only the other platforms and its suggested command keeps --skip-claude.

  • --runtime hermes works with hermes 0.19.0 (#531). The runner ran hermes chat --query-file <file> --oneshot -Q, and hermes 0.19.0 (the PyPI release) has neither flag, so every turn failed with unrecognized arguments. The runner now reads hermes 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-file keeps that form, and otherwise the runner runs hermes 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 in ps or /proc while the turn runs, and a prompt over 128 KiB fails with a clear error instead of E2BIG. 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 to auth, and the rest of stdout is shown but never used to pick the error code.

  • --runtime pi-rpc reports a missing API key at once instead of timing out (#532). Without a key, pi 0.87.1 in --mode rpc answers the prompt with success: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 with missing 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 with pi 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 exec reports a missing API key as error {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" was runner-error; §3.4's auth now 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 into auth.
  • An unknown cost is reported as null, not 0 (#533). A runtime that reports no cost (codex, kimicode, hermes, vercel and the other reports_cost: false runtimes) showed cost_usd: 0 in agent exec's usage and result events, in agent test --json when no tokens were reported, and in org usage events, org report and org costs — the same value as a free turn. Those fields are now null when no cost was reported, and totals are the sum of the reported costs or null when there are none. When only some usage had a cost, the total is a lower bound and org report --json / org costs --json add cost_complete: false. A null cost never counts toward --budget-usd or a role's budget_usd. The human-readable org costs, org report --by-role, org logs --stats, run-end line and org status say unknown; the dashboard's run history says unpriced. vercel and hermes no longer yield a fake cost_usd: 0. Protocol rev 28, capability agent-exec-cost-null.

  • agent models marks aliases of the same model (#534). Claude's default and opus aliases 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 as opus keeps working; an entry that resolves to the same model as an earlier one now carries alias_of with that entry's id ("alias_of":"default" on opus), and the earlier entry lists all of them in aliases. Skip alias_of entries to test each model once. The table view shows (alias of default). What each alias resolves to is unchanged. Capability agent-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-save in the current directory, which created or changed node_modules in 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/deps and loads it from there without a restart. security defend, security redteam and 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, whose exports have only an import condition; before, it could not load one even once installed.

  • design detect <url> and design fix <path> use their positional target (#520). The --target option had a parser default of ., which always won over the positional argument, so monomind design detect https://example.com scanned the current directory instead (and design fix ./src fixed the current directory). The positional target is now used when -t/--target is not given; -t still 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_snapshot feature is on by default in codex 0.156.1, and codex 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=false on every codex exec, so no snapshot is written. Before each spawn it also makes $CODEX_HOME (default ~/.codex) and its sessions/ and shell_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/*.sh and 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 add evil.json with a policy.git: push role plus an evil/run file, and org serve started it with push credentials. A role could also widen its own org's policy for the next reload or run. access_ack covered 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, sandbox allowWrite/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's title, responsibilities and ui, so each role's whole policy, the roles with their runtimes and adapters, instructions_file, skills/skill_pool, run_config (including prechecks), schedule, runtime, fence, federation and loadouts are covered; a __proto__/constructor/prototype key 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. sign refuses inside an org role's or agent exec's process tree; without a TTY and --yes it 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's prechecks run), resume from a checkpoint, and org reload. An unsigned or changed definition is refused with "run monomind 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 a hot-reload-refused audit event. Goal and prompt edits need no new signature.

    • Migration: existing orgs are unsigned. org run on a TTY offers a one-time review and sign; org serve, a detached org run and every other non-TTY path refuse until the operator runs monomind org sign <org>, or monomind org sign --all once per checkout (this includes the shipped release, monomind-dev and sample-team orgs). org create and /mastermind:createorg sign the org they write. org role set-access re-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 as projects/, browser state and models/), the terminal-execution gate, ~/.npm/_npx, ~/.npmrc, ~/.local/bin and more shell startup dirs. They are refused to the file tools, in the SDK sandbox's denyWrite and read-only in the bubblewrap mask; the skill libraries and the terminal gate (as disabled) are created before a role starts. A signed policy.sandbox.allowWrite entry opens one of them. A role can no longer fill npx's cache; point npm_config_cache at $TMPDIR if it must. In the mask ~/.monomind itself is read-only with only those entries bound back, and cline and aider keep an org role's state in its $TMPDIR. mcp stop signals the pid in ~/.monomind/mcp.pid only when its command line is a monomind MCP server, and the terminal-execution gate is read from $MONOMIND_HOME when set, where it is protected. doctor -c org-skills and org sign list org skills that come from the project or user library.

    • instructions_file is 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 with O_NOFOLLOW and 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*.json variants that were not there at monomind's first look (recorded once; a live legacy config the operator already had is left alone), a .mcp.json in 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 killed org run does not make a plant look pre-existing, and only monomind 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-second org run and org serve tick, and every doctor run. 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 exact mv … && monomind org approve-paths … to restore it. The first look prints what it trusted. doctor -c org-skills warns when .mcp.json servers are approved by name but .mcp.json is missing, untracked or modified.

    • org test-loop runs 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 sign and every org start name each unconfined role and why it runs without an OS sandbox (for example policy.git push, and no bubblewrap mask). To confine one, run it on the claude runtime below policy.git: push with 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's PATH (#527). With node from mise, nvm, volta, fnm, asdf or bun, the SDK sandbox's allowWrite (which includes $HOME) and the bubblewrap mask both let a sandboxed role replace node, npm or monomind's own code, or drop a git into a writable PATH directory, 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 of process.execPath, the running CLI's package root, every writable directory on PATH (and the directory holding a symlink on the way to one, such as fnm's multishells), and node/npm/npx/claude on PATH, 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_HOME and 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's rc, Go's env). The list joins the operator-protected paths, so it is refused to the file tools, in the SDK sandbox's denyWrite, 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 own pnpm add -g is 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/bin and ~/.npm/_npx. An org root at $HOME no longer uncovers ~/.monomind in the mask, and excludes no PATH directory (with a warning). Roles still run node, npm and claude. A role that installs a global tool gets EROFS; npm_config_prefix=$TMPDIR/npm-global installs 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_HOME aside (#526, follow-ups to the #518 review).

    • Pinned hashes. The Claude Agent SDK's sdk.mjs, each platform's Claude binary, and every dist/*.js file 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/deps or found up monomind's own module path. Every query() then passes the verified bundled binary as pathToClaudeCodeExecutable, so the SDK never picks its own (an installed Claude Code that #522 selected is passed as is and not pin-checked), and one stat before 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.mjs is 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/browsers is 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 denyWrite path that does not exist, so a role could create ~/.npmrc, ~/.config/npm or 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, ~/.profile as empty files (mode 0600). Empty, each runs or sets nothing: npm merges nothing from it, interactive bash sources nothing, and ~/.profile is read by sh/dash login shells, and by bash only without .bash_profile/.bash_login, never by zsh. ~/.profile is not created when $SHELL is zsh or fish: installers such as nvm's append to ~/.profile when it exists instead of that shell's rc file;
      • ~/.ssh, ~/.config/git, ~/.config/gh, ~/.config/npm as empty mode-0700 directories (and ~/.config if missing). Tools read files inside them, not the directories.

      Linux only. On macOS only ~/.config is 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_profile and ~/.bash_login (bash would skip ~/.profile), ~/.gitconfig (git config --global would 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_HOME and ~/.monomind whose 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 denyWrite entry 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