@getmarrow/mcp 3.9.103 → 3.9.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +43 -6
  2. package/dist/approval-reply.d.ts +2 -0
  3. package/dist/approval-reply.d.ts.map +1 -0
  4. package/dist/approval-reply.js +18 -0
  5. package/dist/approval-reply.js.map +1 -0
  6. package/dist/cli.js +1677 -1636
  7. package/dist/cli.js.map +1 -1
  8. package/dist/codex-plugin-pins.d.ts.map +1 -1
  9. package/dist/codex-plugin-pins.js +18 -0
  10. package/dist/codex-plugin-pins.js.map +1 -1
  11. package/dist/env.d.ts +8 -0
  12. package/dist/env.d.ts.map +1 -1
  13. package/dist/env.js +83 -4
  14. package/dist/env.js.map +1 -1
  15. package/dist/hook-context.d.ts.map +1 -1
  16. package/dist/hook-context.js +25 -0
  17. package/dist/hook-context.js.map +1 -1
  18. package/dist/hook-contract.d.ts +4 -3
  19. package/dist/hook-contract.d.ts.map +1 -1
  20. package/dist/hook-contract.js +43 -6
  21. package/dist/hook-contract.js.map +1 -1
  22. package/dist/hook-pre-action.d.ts +43 -7
  23. package/dist/hook-pre-action.d.ts.map +1 -1
  24. package/dist/hook-pre-action.js +518 -21
  25. package/dist/hook-pre-action.js.map +1 -1
  26. package/dist/hook-tool-policy.d.ts +86 -4
  27. package/dist/hook-tool-policy.d.ts.map +1 -1
  28. package/dist/hook-tool-policy.js +354 -21
  29. package/dist/hook-tool-policy.js.map +1 -1
  30. package/dist/hook.d.ts.map +1 -1
  31. package/dist/hook.js +5 -3
  32. package/dist/hook.js.map +1 -1
  33. package/dist/host-approval-state.d.ts +24 -0
  34. package/dist/host-approval-state.d.ts.map +1 -1
  35. package/dist/host-approval-state.js +68 -0
  36. package/dist/host-approval-state.js.map +1 -1
  37. package/dist/host-approval.d.ts +22 -1
  38. package/dist/host-approval.d.ts.map +1 -1
  39. package/dist/host-approval.js +65 -4
  40. package/dist/host-approval.js.map +1 -1
  41. package/dist/index.d.ts +5 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/keyless-server.d.ts +53 -0
  46. package/dist/keyless-server.d.ts.map +1 -0
  47. package/dist/keyless-server.js +230 -0
  48. package/dist/keyless-server.js.map +1 -0
  49. package/dist/normalized-action.d.ts +11 -3
  50. package/dist/normalized-action.d.ts.map +1 -1
  51. package/dist/normalized-action.js +305 -49
  52. package/dist/normalized-action.js.map +1 -1
  53. package/dist/own-server-identity.d.ts +22 -3
  54. package/dist/own-server-identity.d.ts.map +1 -1
  55. package/dist/own-server-identity.js +108 -31
  56. package/dist/own-server-identity.js.map +1 -1
  57. package/dist/own-tool.d.ts +14 -6
  58. package/dist/own-tool.d.ts.map +1 -1
  59. package/dist/own-tool.js +18 -7
  60. package/dist/own-tool.js.map +1 -1
  61. package/dist/seatbelt/env-dump.d.ts +66 -0
  62. package/dist/seatbelt/env-dump.d.ts.map +1 -0
  63. package/dist/seatbelt/env-dump.js +333 -0
  64. package/dist/seatbelt/env-dump.js.map +1 -0
  65. package/dist/seatbelt/env-names.d.ts +22 -0
  66. package/dist/seatbelt/env-names.d.ts.map +1 -0
  67. package/dist/seatbelt/env-names.js +298 -0
  68. package/dist/seatbelt/env-names.js.map +1 -0
  69. package/dist/seatbelt/fallback-copy.d.ts +16 -0
  70. package/dist/seatbelt/fallback-copy.d.ts.map +1 -0
  71. package/dist/seatbelt/fallback-copy.js +68 -0
  72. package/dist/seatbelt/fallback-copy.js.map +1 -0
  73. package/dist/seatbelt/hook.d.ts +21 -2
  74. package/dist/seatbelt/hook.d.ts.map +1 -1
  75. package/dist/seatbelt/hook.js +45 -11
  76. package/dist/seatbelt/hook.js.map +1 -1
  77. package/dist/seatbelt/mapper.d.ts.map +1 -1
  78. package/dist/seatbelt/mapper.js +1592 -85
  79. package/dist/seatbelt/mapper.js.map +1 -1
  80. package/dist/seatbelt/tables.d.ts +1 -1
  81. package/dist/seatbelt/tables.d.ts.map +1 -1
  82. package/dist/seatbelt/tables.js +13 -17
  83. package/dist/seatbelt/tables.js.map +1 -1
  84. package/dist/secret-paths.d.ts +40 -0
  85. package/dist/secret-paths.d.ts.map +1 -0
  86. package/dist/secret-paths.js +59 -0
  87. package/dist/secret-paths.js.map +1 -0
  88. package/dist/session-loop-guard.d.ts.map +1 -1
  89. package/dist/session-loop-guard.js +25 -6
  90. package/dist/session-loop-guard.js.map +1 -1
  91. package/package.json +2 -2
  92. package/server.json +2 -2
package/README.md CHANGED
@@ -77,6 +77,8 @@ Verify current claims through the [public evidence manifest](https://getmarrow.a
77
77
 
78
78
  ## Install
79
79
 
80
+ **Windows:** Native Windows is not supported yet. Use WSL.
81
+
80
82
  For most installations, follow the [Quick start](#quick-start): the universal installer's `activate` command. The steps below are the manual, MCP-only route.
81
83
 
82
84
  Get a key at [getmarrow.ai/signup](https://getmarrow.ai/signup) and keep it in an owner-only key file: put `MARROW_API_KEY=<your_key>` in `~/.marrow/env`, then run `chmod 700 ~/.marrow && chmod 600 ~/.marrow/env` (Marrow rejects a key file whose folder others can write):
@@ -87,6 +89,8 @@ mkdir -p ~/.marrow && chmod 700 ~/.marrow
87
89
  chmod 600 ~/.marrow/env
88
90
  ```
89
91
 
92
+ Claude Code plugin users: after /plugin install, run /reload-plugins (or restart Claude Code). To add a key, put MARROW_API_KEY=<your key> in ~/.marrow/env, run chmod 600 ~/.marrow/env, then /reload-plugins. Do not run activate or marrow-mcp setup for the plugin, because they write a second copy of the hooks.
93
+
90
94
  Fallback if you cannot use the key file (a key typed on a command line stays in your shell history): set it through trusted secret storage, or `export MARROW_API_KEY=<your_key>` for the session.
91
95
 
92
96
  Hooks-only setup (`marrow-mcp setup` writes hooks and `CLAUDE.md` instructions, and adds no MCP server). It needs the key first: without one it prints how to get it and exits 2 before writing anything.
@@ -157,12 +161,13 @@ A saved `observed_unverified` outcome is terminal observation evidence, not a co
157
161
 
158
162
  When a pre-action hook has no usable Marrow key and the machine shows no sign of a Marrow account (for example an owner key file, a claim marker or Marrow's local approval state), the hook runs a local seatbelt instead of holding every protected action:
159
163
 
160
- - **It stops seven kinds of action:** a change to Marrow's hooks, settings or plugins (including the Marrow plugin switch in Claude Code settings and the plugin files under `~/.claude/plugins`) or an owner-only Marrow command (S0), a force-push or delete of a protected branch (S1), a production deploy (S2), a package publish (S3), reading credentials or secrets, including a project `.env` file other than a template (S4), a recursive delete outside the workspace (S5), and decoded data run by a shell or interpreter (S-x). A protected file named to any program that is not a plain reader (such as `cat`, `grep`, `jq` or `diff`) stops too, so an editor or a tool the seatbelt does not know cannot rewrite it. A tool counts as a plain reader only with the long options it lists, spelled in full: GNU tools and git also accept a shortened spelling (`cp --target=DIR`, `git reset --har`), so a shortened or unknown long option on a command that names a protected file stops. Archiving (`tar -c`, `zip`), `tree` and `prettier --check` only read what they are given: only the archive and the files they write are checked. The stop is the host's own deny, with text that tells the agent to leave the action to the machine owner. On Grok the stop is Grok's fixed denial ("Marrow blocked this protected action."), the only denial its installed guard passes.
164
+ - **It stops seven kinds of action:** a change to Marrow's hooks, settings or plugins (including the Marrow plugin switch in Claude Code settings and the plugin files under `~/.claude/plugins`) or an owner-only Marrow command (S0), a force-push or delete of a protected branch (S1), a production deploy (S2), a package publish (S3), reading credentials or secrets, including a project `.env` file or any other `<name>.env` file that exists (templates such as `.env.example` pass), `~/.marrow/fleet.env` and `~/.marrow/credentials` (S4), a recursive delete outside the workspace (S5), and decoded data run by a shell or interpreter (S-x). A protected file named to any program that is not a plain reader (such as `cat`, `grep`, `jq` or `diff`) stops too, so an editor or a tool the seatbelt does not know cannot rewrite it. A tool counts as a plain reader only with the long options it lists, spelled in full: GNU tools and git also accept a shortened spelling (`cp --target=DIR`, `git reset --har`), so a shortened or unknown long option on a command that names a protected file stops. Archiving (`tar -c`, `zip`), `tree` and `prettier --check` only read what they are given: only the archive and the files they write are checked. The stop is the host's own deny, with text that tells the agent to leave the action to the machine owner. On Grok the stop is Grok's fixed denial ("Marrow blocked this protected action."), the only denial its installed guard passes.
161
165
  - **Everything else gets no Marrow decision, so the host's own approval step decides:** Claude Code and Codex get an empty answer, Cline is not cancelled and Windsurf gets no output. Gemini CLI gets `{"decision":"allow"}`, the only answer its installed guard passes without blocking. Cursor asks with its own approval prompt before a shell command that a hook without a key used to hold. Cursor's other tools (MCP calls included) and Grok have no neutral answer, so they keep holding what a hook without a key held before. An action that a hook without a key already let through gets the same answer as before.
162
166
  - **Host tools and MCP calls:** the seatbelt reads each host's own file tools (for example Cline `write_to_file` and `replace_in_file`, Gemini CLI `read_file`, `replace` and `delete_file`, Grok `search_replace`, Cursor `Delete`) and MCP calls in every host form (`mcp__server__tool`, `MCP:server:tool`, Gemini `mcp_server_tool` and `server__tool`, Cline `use_mcp_tool` and `access_mcp_resource`). Every string value in an MCP call's arguments, at any depth, gets the S0 and S4 path checks. A tool it does not recognise, or MCP arguments it cannot read, never gets the seatbelt's pass: the hook gives the answer a hook without a key gave before, a hold where that held.
163
167
  - **It fails closed:** an internal error, a check that runs past 300 ms, or input the host cut short stops the action. Windsurf and Cline do not tell the hook the working folder, so there a recursive delete outside the temporary folders stops.
164
168
  - **Local receipts:** each seatbelt decision is written to `~/.marrow/seatbelt/receipts/` on this machine, as codes and keyed hashes, plus the built-in tool name and shell program names from fixed lists (any MCP tool is recorded only as `mcp`); no command text, argument, path or URL. Receipts are kept for up to 90 days or 50 MB. Nothing is uploaded. Where the seatbelt enforces (no working key), an agent cannot edit the receipts or read the device salt that keys them (S0, S4); with a working key the seatbelt only watches. Receipts are a record, not an authority.
165
- - **Folder permissions:** the seatbelt runs only where your home folder and the agent settings folders (such as `~/.claude`) can be written by you alone. On Linux, a folder that your own private group can also write (the default with umask 002, mode 0775) still counts. A folder others can write, a group with other members, a folder reached through a symlink, or users and groups from a network directory (LDAP, SSSD) keep every hold, and the hold says "Marrow has no working key on this machine and its keyless mode is off here (strict mode, or a home folder it cannot trust); owner: run `npx -y @getmarrow/install@latest doctor`."
169
+ - **Folder permissions:** the seatbelt runs only where your home folder and the agent settings folders (such as `~/.claude`) can be written by you alone. On Linux, a folder that your own private group can also write (the default with umask 002, mode 0775) still counts. A folder others can write, a group with other members, a folder reached through a symlink, or users and groups from a network directory (LDAP, SSSD) keep every hold, and the hold names the reason and how to fix it, for example "Marrow stopped this action: there is no Marrow key on this machine, and Marrow uses its stricter no-key mode here because HOME is not your login home folder. Fix (for the person at this machine, not the agent): start the agent with HOME set to your login home folder, or add a Marrow key (getmarrow.ai/docs#start-here)."
170
+ - **Stops you may meet on purpose:** `cp .env.example .env.local` (or any command that writes a `.env` file), `set -a && . ./.env && npm run dev` and `docker compose --env-file .env up` are stopped as secrets (S4), and `rm -rf ~/.cache/<app>` as a recursive delete outside the workspace (S5). If you meant it, run the line yourself in your own terminal. Each stop message names the kind of action and ends with its code.
166
171
  - **It is a seatbelt, not a vault:** it runs on the machine as your user and makes no network call. A determined agent can get around a local check, for example with a script it writes now and runs later, an interpreter, or a tool path with no hook.
167
172
  - **With an account:** a machine linked to an account but with no working key keeps every hold it had, and these stops apply too. With a working key nothing changes; the seatbelt only checks the action after Marrow has answered, without changing the answer.
168
173
 
@@ -196,7 +201,9 @@ These behaviors shipped in 3.9.102 and still apply.
196
201
  - **Safer key-file advice:** an owner file that is a symlink, readable or writable by others, owned by another user, or in a directory others can write supplies nothing, and the missing-key message names the file with the `chmod` fix.
197
202
  - **Only Marrow's own tools skip the gate, by exact name:** a tool is Marrow's own only when its server is exactly `marrow` and its tool name is exactly one this package registers. Look-alike servers, other cases, extra spaces or suffixes go through the pre-action gate like any other MCP tool. Re-run `marrow-mcp setup` to refresh hooks written with the old matcher.
198
203
 
199
- ## What's New in v3.9.103
204
+ ## Seatbelt, outage holds and exact approvals
205
+
206
+ These behaviors shipped in 3.9.103 and still apply.
200
207
 
201
208
  - **Keyless seatbelt:** a hook with no key, on a machine with no sign of a Marrow account, stops seven kinds of action (Marrow's own settings, force-push or delete of a protected branch, production deploy, package publish, reading credentials, recursive delete outside the workspace, decoded data run by a shell) in shell commands, each host's own file tools and MCP calls, and leaves everything else to the host's own approval step. A tool it does not recognise keeps the answer a hook without a key gave before. Machines linked to an account keep every hold they had. A Linux home or agent settings folder that only your own private group can also write (umask 002) keeps the seatbelt. See [Keyless seatbelt](#keyless-seatbelt). Marrow's own plugins and the Marrow plugin switch are protected like its settings.
202
209
  - **High-risk actions wait while Marrow is down:** with a key, if Marrow cannot be reached, a git push, publish, deploy, API delete, seatbelt-category command or other high-risk tool (for example an MCP tool) is held on every host until Marrow is back; words in a commit message or a path never hold a command, and routine actions keep running with a warning. If Marrow answers but cannot issue a permit, a high-risk action is denied and never allowed as offline.
@@ -205,7 +212,36 @@ These behaviors shipped in 3.9.102 and still apply.
205
212
  - **Hidden commands are checked:** commands inside an unquoted here-document body, a `${x:-$(...)}` default or an arithmetic expansion are checked like any other command.
206
213
  - **`marrow-mcp keys create` reads every flag form:** `--name=`, `--type=` and `--expires=` (and `-n=`, `-t=`) are read like `--name value`; a flag that is present but blank, or a `--scopes` / `--agents` list with no entry, is a usage error (exit 2) and nothing is sent; leaving `--scopes` and `--agents` out sends neither.
207
214
 
208
- Earlier behaviors are kept under [Project files and Marrow's own tools](#project-files-and-marrows-own-tools), [Setup and key handling](#setup-and-key-handling) and [Hook behavior details](#hook-behavior-details). v3.9.99 brought approvals in the host prompt; see [Approvals in the Host Prompt](#approvals-in-the-host-prompt).
215
+ ## What's New in v3.9.107
216
+
217
+ v3.9.107 is a security hotfix on 3.9.106. The full entries are in CHANGELOG.md.
218
+
219
+ - **Every text hash is keyed per install.** The `command_sha256` and `input_sha256` of every hooked tool (Bash, Monitor, PowerShell, Agent, Edit, Write, WebFetch, MCP tools, and any tool name Marrow does not know, among others), on Claude Code, Codex and Cursor, are now `HMAC-SHA256` under a key derived from the per-install device key (`~/.marrow/seatbelt/salt`), with the tool name and kind inside the MAC. A free-text field can hold a value the redactor cannot see, and a plain hash of it could be guessed offline. The same call on the same install gets the same hash, so a hold still resumes and a learned approval still matches there. A deploy command Marrow sends in clear (`sst deploy --stage production`) keeps its plain hash. `exact_hmac` moves to a `v2` domain. The hold `correlation` and `target` in `~/.marrow/host-approvals/state.json` and the lifecycle `correlation_id` follow.
220
+ - **Upgrade note: some approvals are asked once more.** A hold or a learned approval made before this update never matches again, and an approval learned on one machine does not match on another. A learned approval, an automatic approval or the memory of an earlier decline that the service keyed on a text hash does not carry across machines, or from before this update: the action is asked again, never allowed on a match it no longer has.
221
+ - **A machine with no usable device key can still resume a hold.** With no usable device key (a read-only home, an unsafe, corrupt or missing key file, Windows) the exact-action hashes and the hold target are keyed with a fallback key (HMAC-SHA256 of the secret of `~/.marrow/host-approvals/state.json`) instead of a new random value per call. A held action then resumes, is reported and is committed as on a usable key. When the approval state is unusable too, the action is held and asked every time, with one stderr note. Only a hook with a Marrow key makes that state when it does not exist; a machine with no key never creates it.
222
+ - **Less text is sent in `programs[]`.** The first word of a pipeline stage that feeds a password program (`"pw" | smbpasswd -s`) is no longer sent or stored when it may be the value itself: it is named only when it is a known program (a stage with other words) or one of `cat`, `yes`, `pbpaste` (alone); any other word becomes the fixed marker `unlisted-feeder` and the action is never bound. A tool that does not run shell text (ToolSearch's `query`, a prompt, a path, an unknown tool's `command`) names a first word only when it is a known program; any other word becomes `unlisted-program`. Bash, Monitor, PowerShell, Codex `exec` and Cursor `Shell` are unchanged.
223
+ - **A secret in an edit path is replaced.** A known-format secret inside the path of an edit tool (`Edit`, `Write`, `NotebookEdit`, `MultiEdit`, an `apply_patch` header) is replaced in `normalized_action.paths[]` as a span: `/p/ghp_...X.txt` is sent as `/p/[secret].txt`. An action whose path held a span is never bound (it is asked each time). Not covered: a plain word that is not key-shaped, a token split across `/`, and a prefix written in another case.
224
+ - **More commands that print the environment are stopped.** The environment-dump stop (the keyless S4 stop, and the keyed protected action) now also covers `/proc/<pid>/task/<tid>/environ` read by an input redirect, the Read and Grep tools, a glob, a variable default, a `cd` into a process folder or `find /proc -name environ -exec`, and a bare `env` or `printenv` run by `awk`, by code passed with `-e`, `-c` or `-r`, or by an `sh -c` positional parameter. It only adds stops. It is a known list of forms, not every command a shell can run; the known gaps are listed in CHANGELOG.md.
225
+ - **The approvals state file is protected like the receipts salt.** Reading `~/.marrow/host-approvals/state.json` (with its temp copy and any backup beside it) now gets the decision a read of the receipts salt gets: a keyless S4 stop, a protected keyed action and an outage hold. Listing or stat of the folder and words that only mention the folder are unchanged.
226
+ - **Keyed mode stops secret-file reads hidden in nested shell code.** In keyed mode a read of a secret file (`.env`, `.env.local`, `.env.<name>`, `.npmrc`, `.netrc`, `.dev.vars`, a key file) hidden in `bash -c`, `eval`, `sudo -n sh -c`, `env -S`, `xargs`, `ssh`, a chain, a variable default, a glob or a symbolic link is now a protected action, as the keyless seatbelt already stops it. Keyed mode also treats `.env.dist` and `.env.defaults` as secret files, like keyless mode. Side effect: a `grep .env README.md` or `jq '.env' package.json` inside `bash -c`, `eval`, `ssh` or `sudo -n sh -c` is now stopped in keyed mode.
227
+ - **awk programs that run a stopped action are stopped (keyless).** The keyless seatbelt now stops an awk-family program (`awk`, `gawk`, `mawk`, `nawk`, `busybox awk`, and any other program whose name contains `awk`) that runs a stopped action built at run time. A command made only of string literals is judged as a nested shell; a command with any other part (ARGV, ENVIRON, a `-v` variable, a field, a value read by getline, a function call) is the new dynamic-command stop `dynamic_awk_command`. An awk command that builds its command string from ARGV, a `-v` value or a field is now stopped even when it is harmless. Keyed mode is unchanged.
228
+ - **`command` looks a name up only as a leading `-v` or `-V`.** For every program, `command npm publish -v`, `command git push -v --force origin main` and `command rm -rf ~ -v` passed the keyless seatbelt before and now stop. `command -v node`, `command -V git` and `command node -v` are unchanged.
229
+ - **Installer runtime core (loosens one check).** The installer's next runtime core is now accepted next to the 0.1.70-0.1.74 core, so a HOME behind a symlinked parent installs. On Cursor and Codex, a `marrow` server launched by that new core now proves the official server as the old core's launch does. A launch is still proven only when it is exactly the loader that started the hook, and any other core is not proven. An installer with the new core and an older MCP is not proven and its own tools are held. Release the MCP first, then the installer.
230
+
231
+ These behaviors shipped in 3.9.106 and still apply.
232
+
233
+ v3.9.104 and v3.9.105 were not published to npm; their changes shipped in 3.9.106 and are listed in CHANGELOG.md.
234
+
235
+ - **No key no longer breaks the server.** With no Marrow key anywhere the server reads one, the MCP server now starts with zero tools and an `instructions` text that says how to connect a key (create one at https://getmarrow.ai/signup, then run `npx -y @getmarrow/install@latest activate`), instead of printing "MARROW_API_KEY environment variable is required" and exiting 1, which hosts showed as a broken server. A `tools/call` is a JSON-RPC error: it judges, records, reads and writes nothing. A key of any shape (an invalid or whitespace-only one, `MARROW_KEY`, a fleet `MARROW_KEY_<NAME>`, any `--key` argument) or an owner key file that fails the owner-only check keeps the previous behavior, and a key only in a project `.env` still never counts. Under the Claude Code plugin the instructions carry the plugin's key steps.
236
+ - **Installer-written servers are proven exactly.** On Cursor and Codex, an installer-written `marrow` server skips the gate for Marrow's own tools only when it is exactly the launch of the installer loader that started the hook: the same version, the same MCP pack integrity, the same SDK pin and the same runtime core. An older installer's launch, a look-alike version such as `3.9.1050`, or a launch with no pack integrity now goes through the gate like any other MCP server. The npx and `marrow-mcp` bin forms are unchanged.
237
+ - **Commands that print the environment are stopped.** Commands that print many or all environment variables stop with the existing S4 message without a key, and are protected actions with one. They include `compgen -e`, `tmux show-environment`, `systemctl show-environment`, `ps -E`, `npm run env` (and pnpm, yarn, bun), `php -i`, `strace -v`, `/proc/<pid>/task/<tid>/environ`, `env` run through wrappers such as `setpriv`, `unshare`, `runuser`, `nsenter` and `xargs`, and environment-printing code passed to an interpreter (`node --eval=CODE`, `perl -e`, `python3 -c`, `php -r`).
238
+ - **Every Claude Code tool reaches the hook.** The Claude Code matcher sends every tool call to the hook except Marrow's own exact tool names and the read-only `LS` and `ListMcpResourcesTool`. Before, a fixed list of built-ins reached it, so `Monitor`, `PowerShell`, `WebFetch`, `Agent`, `Skill` and tools added later did not. A `Monitor` command is classified exactly as the same Bash command, keyless and keyed (a keyless `Monitor: git push --force origin master` is stopped, a keyed one is held). A `PowerShell` command is read by the keyless seatbelt as the same Bash command, and keyed as the stricter of its old reading and Bash's reading with none of Bash's read-only lists applied (in PowerShell `type`, `cat`, `find` and `ls env:` read files or print the environment), so a `PowerShell` call is never read-only and `type ./production.env` is held as a secret read. An approval for one never releases the other, and it is bound to the call's `timeout`, `timeout_ms` and `run_in_background`. `Agent`, `WebFetch` and the other tools that only start work, fetch a page or schedule something are recorded, but the words in their prompt or URL never hold them. The same goes for the built-ins that only report, notify, structure output or render (`PushNotification`, `ReportFindings`, `StructuredOutput`, `Artifact`, `ArtifactCheck`, `ArtifactComments`, `ArtifactData`, `ListAgents`, `SubagentHandback`), and `Workflow`'s `script` is read as JavaScript, never as shell text; a tool Marrow does not know keeps the word check, and `RemoteTrigger` and `DesignSync` stay named. What these tools lead to is hooked on its own. Re-run `marrow-mcp setup` (or the installer) to refresh hooks written with the old matcher: `setup` recognises the hooks the installer writes, so running it over an installer-written settings file leaves exactly one Marrow entry per event, and a look-alike hook of your own is kept as it is.
239
+ - **More secret files are protected.** With no key, reading or copying any `<name>.env` file that exists (for example `prod.env`), `~/.marrow/fleet.env` or anything under `~/.marrow/credentials` is stopped like `.env` (S4). `git log -p`, `git whatchanged -p` and `git format-patch` of a secret path print its history, so they are stopped like `git show -- <path>`; `git log --oneline -- .env` and `git log -p -- src/app.ts` still pass. When the hook does not know the working directory, a relative `<name>.env` read (`cat prod.env`, `cat config/`) is stopped too; a search pattern such as `grep -rn "process.env" src` still passes. Templates (`.env.example`, `.env.sample`, `.env.template`) and plain words such as `process.env` in a search still pass. With a key, the check now reads the same list, so `~/.marrow/env` and `env.local`, `~/.claude.json`, the GitHub CLI `hosts.yml`, the 1Password config and the seatbelt salt count as secret reads there too.
240
+ - **A URL in the environment cannot redirect your key.** A key from `~/.marrow/env` (or `env.local`) is sent only to the default Marrow endpoint, to the endpoint that same file names with `MARROW_BASE_URL`, or to an exact origin it lists in the new `MARROW_ALLOWED_BASE_URLS`. A `MARROW_BASE_URL` that only the process environment names (a project's `.mcp.json` or settings `env` block, direnv, a devcontainer) no longer moves it: the default endpoint is used and one line on stderr names the source that was ignored, never the key or the URL. A key from the process environment keeps using the process-environment URL, as before. If you run a staging or self-hosted endpoint with a key in `~/.marrow/env`, add `MARROW_BASE_URL=` or `MARROW_ALLOWED_BASE_URLS=` to that file. See [Environment](#environment).
241
+ - **Ids in tool arguments are checked.** The ids and slugs that become part of an API path (workflow, instance, lease, comparison, handoff, decision, detected and template ids) must start with a letter or digit, use only letters, digits, `_`, `.` and `-`, be at most 256 characters and never contain `..`. Anything else is refused before any request is sent. Every id Marrow itself issues passes.
242
+ - **The SDK is pinned exactly.** `@getmarrow/mcp` depends on `@getmarrow/sdk` 3.7.67 with no version range.
243
+
244
+ Earlier behaviors are kept under [Seatbelt, outage holds and exact approvals](#seatbelt-outage-holds-and-exact-approvals), [Project files and Marrow's own tools](#project-files-and-marrows-own-tools), [Setup and key handling](#setup-and-key-handling) and [Hook behavior details](#hook-behavior-details). v3.9.99 brought approvals in the host prompt; see [Approvals in the Host Prompt](#approvals-in-the-host-prompt).
209
245
 
210
246
  Earlier release notes moved from this README to [CHANGELOG.md](https://github.com/getmarrow/marrow-mcp/blob/master/CHANGELOG.md).
211
247
 
@@ -358,7 +394,7 @@ When Marrow holds an action for approval (`review_required`), the person approve
358
394
  - **Held actions at the next session.** At the first prompt of an interactive session the person sees "N held actions are waiting for you", with the action type and agent only, and approves one by retrying it there.
359
395
  - **One approval, one run.** Of two identical calls on one approval, one runs and the other is denied.
360
396
  - **The exact action, never its text.** Hook-classified calls send `normalized_action`: the tool kind and name, program names (and edit paths, with the home directory as `~`) and a SHA-256 of a normalized form. Event and correlation ids come from that normalized form, never from the raw command, input or prompt. Credentials are replaced where they stood, data a command reads is hashed so `SELECT` and `DROP` are different actions, and anything else that may be secret is withheld. An action that carried a credential or a withheld value is marked `truncated: true`: Marrow never learns a reusable approval for it, and the answer to a held one resumes only that exact action (see How approvals bind). The verdict, the hold and the prompt are the same as without the mark. A credential value ends only at whitespace, a quote, or a separator followed by another `name=` field, so actions that differ after it stay different actions and no part of a password with `#`, `,`, `&` or `;` in it stays in the hashed form. A password given as a plain argument to an unknown program (`./deploy.sh hunter2`) cannot be recognized; it stays only inside the hash.
361
- - **How approvals bind.** Marrow binds an owner approval to the exact action that was held. The client hides possible secrets before sending an action; when it hides anything, it adds `exact_hmac`, a keyed hash of the exact action made with this machine's private device key (`~/.marrow/seatbelt/salt`, created once, mode 0600, never sent). An approval resumes only for the same exact action; a retry that differs only in a certain credential (for example a new one-time password) still resumes. If the device key is missing or unsafe, Marrow prints one note (it points to the installer's `npx -y @getmarrow/install@latest doctor`) and asks again instead of resuming.
397
+ - **How approvals bind.** Marrow binds an owner approval to the exact action that was held. The client hides possible secrets before sending an action; when it hides anything, it adds `exact_hmac`, a keyed hash of the exact action made with this machine's private device key (`~/.marrow/seatbelt/salt`, created once, mode 0600, never sent). An approval resumes only for the same exact action; a retry that differs only in a certain credential (for example a new one-time password) still resumes. If the device key is missing or unsafe, Marrow keys the same hashes with a fallback key derived from the secret of its local approval state (`~/.marrow/host-approvals/state.json`, mode 0600, never sent; a hook with a Marrow key makes it when missing, a machine with no key never does), so a held action still resumes; only when that state is unusable too does Marrow print one note (it points to the installer's `npx -y @getmarrow/install@latest doctor`) and ask again instead of resuming.
362
398
  - **Upgrade note:** the action hash version changed (v3). Learned approvals and known workflows from earlier versions ask once more, then are learned again.
363
399
  - **Slow and down are different.** Codex and the 5-second hosts answer within their hook limits (Codex within 2 s of starting, and on Linux about 2.6 s from launch when npx starts in under about 1.6 s). A slow Marrow keeps a holdable action held. A real outage (Marrow never answered) holds a shell command only when it pushes with git, publishes a package or image, deploys, deletes through an API or a cloud CLI, or hits a seatbelt category (S0-S5, S-x); words in a commit message, a path, echo text or a comment never hold it, and neither does asking a listed tool for its help (`git push --help`). Other high-risk tools (for example MCP tools) are held, routine actions keep running with a warning, and a waiting hold or owner-locked category stays held. When Marrow answers but cannot issue a permit, a high-risk action is denied, never allowed as offline.
364
400
  - **Honest labels and texts.** Without a dialog or typed-reply marker an approval is recorded as the host's allow rule. Text a person reads names no receipt ids. A refused action names a plain fix.
@@ -450,7 +486,8 @@ See the [Trust Center](https://getmarrow.ai/trust/) for implemented controls, cu
450
486
  | Variable | Required | Purpose |
451
487
  | --- | --- | --- |
452
488
  | `MARROW_API_KEY` | Yes | Account or agent-bound API key |
453
- | `MARROW_BASE_URL` | No | API base override |
489
+ | `MARROW_BASE_URL` | No | API base override. When the key comes from `~/.marrow/env`, a `MARROW_BASE_URL` set only in the process environment is ignored (with one notice) unless the same file names it or lists its origin in `MARROW_ALLOWED_BASE_URLS`; a key from the process environment keeps the process environment URL |
490
+ | `MARROW_ALLOWED_BASE_URLS` | No | Read from `~/.marrow/env` only. Comma-separated exact origins (`https://host[:port]`, or `http://localhost`, `http://127.0.0.1`, `http://[::1]`) that a key stored in that file may be sent to when the process environment names them. No wildcards, paths, credentials, queries or fragments |
454
491
  | `MARROW_AGENT_ID` | No | Bound agent identity for MCP tools |
455
492
  | `MARROW_FLEET_AGENT_ID` | No | Fleet agent identity used by passive setup |
456
493
  | `MARROW_HOOK_BACKGROUND_NUDGE` | No | Default on: PostToolUse only spools the event and starts one detached background process that delivers it, so the hook adds almost no latency. Set to `false` to keep bounded inline delivery (750 ms) inside the hook. Never runs when `MARROW_AUTO_HOOK=false` or local control is disabled |
@@ -0,0 +1,2 @@
1
+ export declare function looksLikeApprovalReply(prompt: unknown): boolean;
2
+ //# sourceMappingURL=approval-reply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-reply.d.ts","sourceRoot":"","sources":["../src/approval-reply.ts"],"names":[],"mappings":"AASA,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAI/D"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.looksLikeApprovalReply = looksLikeApprovalReply;
4
+ // Does a short chat message look like an answer to an approval question ("yes", "go ahead")?
5
+ //
6
+ // Used only to show the user one line saying that a chat reply is not an approval (tier A3). The
7
+ // answer is a yes or no and nothing else: the text is never stored, logged, sent or returned.
8
+ const MAX_REPLY_LENGTH = 64;
9
+ // A longer message is not an answer; checked before the text is lowercased or trimmed.
10
+ const MAX_SCAN_LENGTH = 256;
11
+ const APPROVAL_REPLY = /^(?:y|yes|yep|yeah|ok|okay|sure|approve|approved|approve it|i approve|allow|allowed|allow it|confirm|confirmed|go|go ahead|do it|proceed|lgtm|ship it)[.! ]*$/;
12
+ function looksLikeApprovalReply(prompt) {
13
+ if (typeof prompt !== 'string' || prompt.length > MAX_SCAN_LENGTH)
14
+ return false;
15
+ const text = prompt.trim().toLowerCase();
16
+ return text.length > 0 && text.length <= MAX_REPLY_LENGTH && APPROVAL_REPLY.test(text);
17
+ }
18
+ //# sourceMappingURL=approval-reply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-reply.js","sourceRoot":"","sources":["../src/approval-reply.ts"],"names":[],"mappings":";;AASA,wDAIC;AAbD,6FAA6F;AAC7F,EAAE;AACF,iGAAiG;AACjG,8FAA8F;AAC9F,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,uFAAuF;AACvF,MAAM,eAAe,GAAG,GAAG,CAAC;AAC5B,MAAM,cAAc,GAAG,+JAA+J,CAAC;AAEvL,SAAgB,sBAAsB,CAAC,MAAe;IACpD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,eAAe;QAAE,OAAO,KAAK,CAAC;IAChF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACzC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,gBAAgB,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzF,CAAC"}