From 2953b8a8f4c80f19b6e93f8d19ac3529b5942f59 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 02:30:11 +0200 Subject: [PATCH 1/6] Issue 340: an agent can plant a hook in every agent session on the mesh Record the hole the review traced, why the module cannot tell who called, which kinds run or are obeyed in a session and which stay registrable, the fix in the controller and the module, what is registered today, and that it must land before ADR 0266's agent account is switched on. --- .../00-report.md | 99 +++++++++++++ .../01-diagnosis.md | 139 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md create mode 100644 04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md new file mode 100644 index 00000000..adc91852 --- /dev/null +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md @@ -0,0 +1,99 @@ +--- +status: located +opened: 2026-10-09 +located-in: [mesh-catalog (modules/claude-code), mesh-controller (internal/catalogue/terminal_keys.go)] +fixed-by: +amended-design: +--- + +# 340. An agent can plant a hook in every agent session on the mesh + +**Severity: critical.** It is a path from an agent confined on the control node to root on every other node. + +## Symptom + +A review on 2026-10-09 traced this in the code. Nothing was run against a node. + +The `claude-code` module registers the agent's configuration at three scopes +([ADR 0216](../../02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md)). +Its tools take a hook, managed settings, a permission rule and a tool server at the **mesh** and **node** +scopes: `claude_code_hook_register`, `claude_code_settings_set`, `claude_code_permission_add` and +`claude_code_mcp_register`. What they register becomes the shared plugin's hook file, the managed settings +file and the managed tool-server file on every node it reaches. The renderer takes out only the key-helper. +Every Claude Code session there loads these files, as whoever starts it. + +The tool runner acts for any caller of the mesh MCP server, and it is not told who called +([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), +[ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)). So an agent +confined on the control node can register a `SessionStart` hook for every node. The next time the operator +starts `claude` on the laptop, the workstation or the home server, the hook runs as the operator, whose +account has passwordless sudo there. + +Two more routes write the same files: + +- **The controller's settings verb.** The module's `managed_settings` and `mcp_servers` settings land in a + mergeable file. [Issue 339](../339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md) made a + setting the terminal's alone when a trusted file asks for it with a placeholder. A mergeable file takes + its keys without one, so `mesh-controller.settings {module: claude-code, values: {"managed_settings": + {"hooks": …}}}` was taken from any caller. +- **Items whose text runs something.** A skill or a slash command may hold shell lines that the agent runs + when it loads the item, before any permission is asked. A skill's front matter may hold hooks and tools it + pre-approves. A subagent placed in a home may start a tool server or change the permission mode from its + front matter. These kinds are registered at every scope, the home included. + +## Why it is an issue + +ADR 0266 (still on its own pull request, hq 225, so not linked here) moves agents on the control node +to an account of their own that cannot become root without a person. This route leaves that boundary +sideways: the agent does not become root on the control node, but plants what runs as root's equal on every +other node. + +[ADR 0216](../../02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md) +§7 assumed the vendor's guard stops an agent from loosening its own settings. That guard judges a session's +own settings file. It does not see a tool call that changes the managed files of another node. + +## Impact + +- **Any caller of the mesh MCP server could run a command as the operator on every node running the agent**, + at the operator's next session there. That includes every agent session, on the control node too. +- **Whether anyone did is not known for certain, but nothing shows it.** On 2026-10-09, read-only: + - no hook, no settings and no permission rule was registered through the module; + - one tool server was registered, for the laptop alone: a program in the operator's own home, which the + operator made; + - three instruction sections were registered for the mesh; + - no node's layer of the module's settings set `managed_settings` or `mcp_servers`, and no layer had ever + been replaced. + +## Ordering + +**This must land before ADR 0266's agent account is switched on** (hq pull request 225). Until it lands, the +agent account's boundary does not hold, whatever the node-engine judges of the account itself. + +## The acceptance test (the operator's view) + +No agent session can change what runs in another person's or node's Claude Code session. Hooks, +permissions, tool servers and managed settings at the mesh or node scope can only be registered or changed +at the controller's terminal. An agent's attempt through any mesh tool is refused and says why. + +## How it is checked + +- **In the module's tests, through its real tool dispatch.** The test serves the module's tools the way the + runtime speaks to them and calls each one: a hook, settings with a hook, a status line or a permission + mode, a permission rule added or removed, a tool server by command or by address, and a skill, command or + subagent with a shell line or with `hooks`, `allowed-tools`, `mcpServers` or `permissionMode` in its front + matter, at the mesh, node and home scopes. Each is refused with an error that names this issue, and + nothing is put on the bus or placed in a home. On the commit before the fix every one of them was taken. +- **In the module's render tests.** A hook, settings or a skill with a shell line already on the bus, or + kept in the module's own file by an older build, is never rendered. The tool server and the instruction + sections registered before are still rendered, may be unregistered, and are listed with who registered + them. The operator's `managed_settings`, hooks and permission rules included, are what the managed + settings file holds. +- **In the controller's tests, through the real verb dispatch.** Setting, changing, dropping or clearing + the module's `managed_settings`, `mcp_servers` or `role` through the `settings` verb, for one machine or + the mesh, and through the `command` verb, is refused with an error naming this issue. The layer is + unchanged after every refusal. At the terminal the same is taken. These tests fail on the commit before + the fix. +- **Live, after both are taken:** the calls in the diagnosis's last section are each refused, and + `claude_code_registrations` lists what it listed on 2026-10-09. + +The diagnosis is in [01-diagnosis.md](01-diagnosis.md). diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md new file mode 100644 index 00000000..68c74db8 --- /dev/null +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md @@ -0,0 +1,139 @@ +# 340 — Diagnosis + +## 2026-10-09 + +**The records first.** `records_search` for `claude_code_hook_register` and for the module's phrase "refuses +to loosen its own settings" found nothing. The rule the tools rested on is in +[ADR 0216](../../02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md) +§7 and [ADR 0213](../../02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md) +option 3: the operator sets the managed settings through the controller's settings verb, and the vendor's +guard refuses an agent that loosens its own. Neither asks who may call the verb or the tools, nor whether the +settings reach a session other than the caller's. + +### Can the module tell who called? + +No. The runtime hands a bundle a `tools/call` with a tool's name and its arguments, nothing else (the SDK's +`ServeOn`). The runtime answers for any account the mesh MCP server serves, so the operator's own session and +an agent's reach a tool the same way. ADR 0266 §7 says the same of the tool runner. The only signal the mesh +has that a person typed a line is the controller's: a command a verb runs carries the verb in its environment +(`MESH_VERB`, issue 327), and a line typed at the terminal does not. Issue 339 built its terminal-only +settings on that signal. A terminal-only command line in the module was considered and not taken: it would +need the module's bus credential at the operator's terminal on every node, and it would duplicate what the +controller's settings already are. + +So the robust signal is the controller's terminal, and the module's own tools refuse. + +### What a session runs or obeys, by kind + +What the module writes, and whether it runs without the session's own permission check (the vendor's +documentation of skills, subagents, plugins, output styles and memory, read 2026-10-09): + +| Kind | Where it lands | Runs, or decides what runs | Decision | +|---|---|---|---| +| hook | the plugin's hook file | runs its command on its event, as the session's account | terminal only, in `managed_settings` | +| settings, permission rules | the managed settings file | permissions, permission mode, hooks, the status line command, the environment, the key-helper | terminal only, in `managed_settings` | +| tool server | the managed tool-server file | a stdio server is a program the session starts; an http server is handed what the agent asks | terminal only, in `mcp_servers` | +| skill, slash command | the plugin, or a home | shell lines (`` !`…` `` and blocks opened with ```` ```! ````) run when the item loads, before any permission is asked; `allowed-tools` pre-approves tools; a skill's `hooks` run | refused when it holds any of these | +| subagent | the plugin, or a home | in a home, `hooks`, `mcpServers` and `permissionMode` are honoured; in a plugin they are ignored | refused when it holds any of these | +| output style, instructions, rule file | the plugin, the managed instruction file, a home | text the model reads; no field runs anything | still registered | +| a script beside a skill | the plugin, or a home | only when the agent runs it, through the session's own rules and the guard | still registered | + +**Text is a lower class, and stays registrable.** Instructions, output styles and skills steer the agent, as +any text in its context does. A session still asks before it runs what the text suggests, under the +operator's permission rules and the mesh's guard. A skill is also invoked by the model on its own, which is +why its shell lines and pre-approved tools are refused. What the text alone asks for is judged by the +session. Blocking all registered text would end the shared instructions the mesh relies on, and would not +stop text reaching an agent by other routes (a file in a repository, a tool's answer). This is a judgement, +and it is said: a session in a mode that runs everything without asking would run what a registered text +asks for. That mode is the operator's own choice for their own session. + +### The routes that write the same files + +- **The module's tools**: refused (below). +- **The controller's settings verb**: `TerminalKeys` (`internal/catalogue/terminal_keys.go`) counted the + settings a file asks for by placeholder alone. A mergeable file asks for every key it holds without one. + The module's settings file is mergeable, so `managed_settings`, `mcp_servers` and `role` were the verb's. +- **The catalogue's defaults**: the module's settings file holds empty values only. No layer sets them in + the catalogue. +- **The bus**: the module's `config` and `servers` state. Only a holder of the module's grants writes them, + through the tool runner's credential. On the control node, under ADR 0266, the agent account cannot read + it. On the other nodes an agent runs as the operator already. The module still judges every item it takes + from the state, so a hook, settings or an item that runs something is never rendered, whoever put it there. +- **The `nox-mesh` plugin** is the module's own render. It carries the guard's hook and nothing else that runs. + +### The fix + +**In the controller** (pull request in `mesh-controller`): + +1. A mergeable file asks for every key its own content names. Unless the file says `"trusted": false`, each + is a terminal key, refused through every verb like the keys issue 339 named. For the agent's module, + `managed_settings`, `mcp_servers` and `role` are the terminal's. +2. The refusal names this issue beside 339. + +This reaches every mergeable file whose content names keys, not only the agent's. On 2026-10-09 that was 12 +more files in the catalogue: the database web app, the agent's licence manager, the dashboards, the agent +memory store, the chat client's look, the watcher's and the messenger's chat ids, the flow editor, the route +adapter, the search engine, the database pooler and the network controller. In the media catalogue it was 5: +the library sorter and four download clients' settings. Through a verb these keys were the caller's before. +Many decide what a consumer trusts or reaches: a route, a chat id that verifies a sender, a password. +`module check`'s count of files that do not say whether they are trusted rose from 9 to 22 over the catalogue. An author opts a +harmless one out with `"trusted": false`. That waits until the node-engine accepts the field, as issue 339 +says. + +A key the file does not name still lands through a verb. Nothing reading the agent's file reads such a key. + +**In the module** (pull request in `mesh-catalog`): + +1. `claude_code_hook_register`, `claude_code_settings_set`, `claude_code_permission_add`, + `claude_code_permission_remove` and `claude_code_mcp_register` refuse whatever they are given. The error + says why and gives the terminal's line. The tools stay, so an agent that calls one learns why. +2. An item of the kinds `hook` and `settings` is refused at registration, never taken from the state, and + never rendered. The plugin's hook file holds the guard alone. The managed settings file holds the + operator's `managed_settings` under the mesh's own keys. +3. A skill, subagent or command holding a shell line, or `hooks`, `mcpServers`, `permissionMode` or + `allowed-tools` in its front matter, is refused at every scope, and the import tool refuses one too. +4. What the module kept in its own file before is judged again when it renders. +5. Removing stays: unregistering a tool server, a hook or settings takes away what runs. +6. A new read-only tool, `claude_code_registrations`, lists every registration with its `by`, `at`, and + whether it is still rendered and why not, beside what the terminal set for the node. `by` is the node + whose tool runner served the call. The caller was never recorded, because the runner is not told. A tool + server's registration kept no `by`. + +### Existing registrations + +Read on 2026-10-09 through the module's list tools and the settings verb, on all four nodes: + +- three instruction sections for the mesh, registered through the laptop: still rendered; +- one tool server for the laptop alone, a program in the operator's own home: still rendered; +- no hook, no settings, no permission rule; +- no layer of the module's settings sets `managed_settings` or `mcp_servers` on any node, and none was ever + replaced. + +Nothing is stranded by the fix. Should the operator want the tool server under the terminal's care, it can be +set in that node's `mcp_servers` and then unregistered. + +### The decisions this departs from + +ADR 0213 option 3 named the controller's settings verb as the operator's route, and ADR 0216 §2 and §7 +registered hooks, settings and permission rules through the module's tools. Both rested on a premise that +does not hold: that the verb's or the tool's caller is the operator, or that the vendor's guard covers +another node's session. The fix keeps what they decided (one managed-settings setting; one plugin) and moves +who may write it to the terminal. A decision record saying so, and the amendment to to-be 36's checks (its +render test row names a registered hook and setting landing in the plugin), follow this issue through +playbook 02. + +### The acceptance test, live, after both are taken + +Each of these must be refused, saying why, and change nothing: + +- `/claude-code.claude_code_hook_register {name: x, event: SessionStart, command: "true"}`; +- `/claude-code.claude_code_settings_set {settings: {"model": "x"}}`, and with `scope: node, nodes: all`; +- `/claude-code.claude_code_permission_add {list: allow, rule: Bash}`; +- `/claude-code.claude_code_mcp_register {name: x, url: "https://example.org/mcp", nodes: all}`; +- `/claude-code.claude_code_skill_register {name: x, content: "---\nname: x\nallowed-tools: Bash\n---\nx"}`; +- `mesh-controller.settings {module: claude-code, values: "{\"managed_settings\": {\"model\": \"x\"}}"}`, and + with `node`, and `clear: "true"`; +- `mesh-controller.command {command: "settings set claude-code '{\"mcp_servers\":{}}' --node "}`. + +Then `claude_code_registrations` lists what this section lists, and `claude_code_settings_get` on each node +shows the managed settings file unchanged. From 1dbfb2d85a4b6beb30738229e222c7898bc2073c Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 02:55:23 +0200 Subject: [PATCH 2/6] Issue 340: the whole layer of a module with a mergeable file is the terminal's, and the text still registrable is a risk said The review found that a mergeable file takes any key, not only those it names, and that the text a session reads stays registrable through a verb; say both, and the follow-ups the module took. --- .../00-report.md | 21 ++++- .../01-diagnosis.md | 81 ++++++++++++------- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md index adc91852..5bcc36bf 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md @@ -34,8 +34,10 @@ Two more routes write the same files: - **The controller's settings verb.** The module's `managed_settings` and `mcp_servers` settings land in a mergeable file. [Issue 339](../339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md) made a setting the terminal's alone when a trusted file asks for it with a placeholder. A mergeable file takes - its keys without one, so `mesh-controller.settings {module: claude-code, values: {"managed_settings": - {"hooks": …}}}` was taken from any caller. + any key without one, so `mesh-controller.settings {module: claude-code, values: {"managed_settings": + {"hooks": …}}}` was taken from any caller. The review found the same for every module with a mergeable + file: an identity provider's empty runtime configuration took a `url` of the caller's, and its provisioner + would send its admin login there. - **Items whose text runs something.** A skill or a slash command may hold shell lines that the agent runs when it loads the item, before any permission is asked. A skill's front matter may hold hooks and tools it pre-approves. A subagent placed in a home may start a tool server or change the permission mode from its @@ -64,6 +66,13 @@ own settings file. It does not see a tool call that changes the managed files of - no node's layer of the module's settings set `managed_settings` or `mcp_servers`, and no layer had ever been replaced. +## What stays open + +Text a session reads (instruction sections, output styles, and skills, commands and subagents that run +nothing themselves) is still registered through a verb at the mesh and node scopes. It can ask the operator's +agent on every node to run something, which the session then asks about. The diagnosis says what that risk is. +The operator is deciding whether it becomes terminal-only too. + ## Ordering **This must land before ADR 0266's agent account is switched on** (hq pull request 225). Until it lands, the @@ -81,7 +90,9 @@ at the controller's terminal. An agent's attempt through any mesh tool is refuse runtime speaks to them and calls each one: a hook, settings with a hook, a status line or a permission mode, a permission rule added or removed, a tool server by command or by address, and a skill, command or subagent with a shell line or with `hooks`, `allowed-tools`, `mcpServers` or `permissionMode` in its front - matter, at the mesh, node and home scopes. Each is refused with an error that names this issue, and + matter, at the mesh, node and home scopes. Front matter hidden behind a byte-order mark, written as a flow + mapping or with merged keys, and front matter that does not parse, are refused, and so is an `@` reference + to a file outside the item. Each is refused with an error that names this issue, and nothing is put on the bus or placed in a home. On the commit before the fix every one of them was taken. - **In the module's render tests.** A hook, settings or a skill with a shell line already on the bus, or kept in the module's own file by an older build, is never rendered. The tool server and the instruction @@ -90,7 +101,9 @@ at the controller's terminal. An agent's attempt through any mesh tool is refuse settings file holds. - **In the controller's tests, through the real verb dispatch.** Setting, changing, dropping or clearing the module's `managed_settings`, `mcp_servers` or `role` through the `settings` verb, for one machine or - the mesh, and through the `command` verb, is refused with an error naming this issue. The layer is + the mesh, and through the `command` verb, is refused with an error naming this issue. So is a new `url` on a + module whose mergeable runtime configuration starts empty, while a file marked `"trusted": false` stays the + verb's. The layer is unchanged after every refusal. At the terminal the same is taken. These tests fail on the commit before the fix. - **Live, after both are taken:** the calls in the diagnosis's last section are each refused, and diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md index 68c74db8..6d8403d1 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md @@ -38,21 +38,36 @@ documentation of skills, subagents, plugins, output styles and memory, read 2026 | output style, instructions, rule file | the plugin, the managed instruction file, a home | text the model reads; no field runs anything | still registered | | a script beside a skill | the plugin, or a home | only when the agent runs it, through the session's own rules and the guard | still registered | -**Text is a lower class, and stays registrable.** Instructions, output styles and skills steer the agent, as -any text in its context does. A session still asks before it runs what the text suggests, under the -operator's permission rules and the mesh's guard. A skill is also invoked by the model on its own, which is -why its shell lines and pre-approved tools are refused. What the text alone asks for is judged by the -session. Blocking all registered text would end the shared instructions the mesh relies on, and would not -stop text reaching an agent by other routes (a file in a repository, a tool's answer). This is a judgement, -and it is said: a session in a mode that runs everything without asking would run what a registered text -asks for. That mode is the operator's own choice for their own session. +**Text is a lower class, and stays registrable through a verb. This is a risk, and it is the operator's to +take or close.** At the mesh and node scopes, an instruction section, an output style, and a skill, command or +subagent without the fields above are still registered through the module's tools, so any caller of the mesh +MCP server can register them, an agent confined on the control node included. They reach every session on the +nodes they name, as text the model reads: + +- the instruction sections land in the managed instruction file, which every session reads first and treats + as the mesh's own word; +- a skill is invoked by the model on its own when its description matches what it is doing. + +So a registered text can ask the operator's agent, on every node, to run a command. Nothing in it runs by +itself: the session still asks before it runs what the text suggests, under the operator's permission rules and +the mesh's guard. But a session in a mode that runs without asking, or a person who approves what looks like the +mesh's own instruction, would run it as the operator. That is the same sideways path as a hook, made one +approval longer. + +What closing it costs: the mesh's shared instructions and skills, which agents keep up today, would be +registered at the controller's terminal alone. It also does not stop text reaching an agent by other routes +(a file in a repository, a tool's answer). On 2026-10-09 the operator is deciding whether this text becomes +terminal-only too. This issue records the answer when it is given. ### The routes that write the same files - **The module's tools**: refused (below). - **The controller's settings verb**: `TerminalKeys` (`internal/catalogue/terminal_keys.go`) counted the - settings a file asks for by placeholder alone. A mergeable file asks for every key it holds without one. - The module's settings file is mergeable, so `managed_settings`, `mcp_servers` and `role` were the verb's. + settings a file asks for by placeholder alone. A mergeable file takes every key a layer sets, named in its + content or not. The module's settings file is mergeable, so `managed_settings`, `mcp_servers` and `role` were + the verb's. The review found the same for every other mergeable file: 13 runtime configurations start empty, + and a provider reads what lands in them. A `url` set through a verb on the identity provider's would have + had its provisioner send the admin login to the caller's listener. - **The catalogue's defaults**: the module's settings file holds empty values only. No layer sets them in the catalogue. - **The bus**: the module's `config` and `servers` state. Only a holder of the module's grants writes them, @@ -65,22 +80,22 @@ asks for. That mode is the operator's own choice for their own session. **In the controller** (pull request in `mesh-controller`): -1. A mergeable file asks for every key its own content names. Unless the file says `"trusted": false`, each - is a terminal key, refused through every verb like the keys issue 339 named. For the agent's module, - `managed_settings`, `mcp_servers` and `role` are the terminal's. -2. The refusal names this issue beside 339. +1. A module with a mergeable file not marked `"trusted": false` has its **whole layer** set at the terminal. + Through any verb, any change to it is refused, whatever the key, a key no file names included. Reading it + through a verb still answers. For the agent's module that covers `managed_settings`, `mcp_servers` and + `role`. +2. A mergeable file's own keys are also counted as keys it asks for, so `module check` names the file when it + does not say whether it is trusted. +3. The refusal names this issue beside 339. -This reaches every mergeable file whose content names keys, not only the agent's. On 2026-10-09 that was 12 -more files in the catalogue: the database web app, the agent's licence manager, the dashboards, the agent -memory store, the chat client's look, the watcher's and the messenger's chat ids, the flow editor, the route -adapter, the search engine, the database pooler and the network controller. In the media catalogue it was 5: -the library sorter and four download clients' settings. Through a verb these keys were the caller's before. -Many decide what a consumer trusts or reaches: a route, a chat id that verifies a sender, a password. -`module check`'s count of files that do not say whether they are trusted rose from 9 to 22 over the catalogue. An author opts a -harmless one out with `"trusted": false`. That waits until the node-engine accepts the field, as issue 339 -says. - -A key the file does not name still lands through a verb. Nothing reading the agent's file reads such a key. +This reaches every module with a mergeable file, not only the agent's. On 2026-10-09 that was 25 modules in the +catalogue and 11 in the media catalogue, every one with a file that says nothing about trust. Through a verb +their settings were any caller's before. Many decide what a consumer trusts or reaches: a provider's address, a +route, a chat id that verifies a sender, a password. From now on the operator sets them at the terminal. An +author gives a verb back a file nothing trusts with `"trusted": false`. That waits until the node-engine accepts +the field, as issue 339 says. `module check`'s count of files that do not say whether they are trusted rose from +9 to 22 over the catalogue. It counts the files whose content names keys. A file that starts empty is not +counted, but is refused all the same. **In the module** (pull request in `mesh-catalog`): @@ -92,9 +107,15 @@ A key the file does not name still lands through a verb. Nothing reading the age operator's `managed_settings` under the mesh's own keys. 3. A skill, subagent or command holding a shell line, or `hooks`, `mcpServers`, `permissionMode` or `allowed-tools` in its front matter, is refused at every scope, and the import tool refuses one too. -4. What the module kept in its own file before is judged again when it renders. -5. Removing stays: unregistering a tool server, a hook or settings takes away what runs. -6. A new read-only tool, `claude_code_registrations`, lists every registration with its `by`, `at`, and + Front matter is read as YAML, after a byte-order mark, in flow style and with merged keys. Front matter + that does not close, does not parse, holds a key twice or is not a mapping is refused rather than guessed + at. +4. A skill or command that refers with `@` to a file that is not one of its own files is refused. The agent + reads such a file into its prompt when the item loads, so the item would carry another file's words, or a + secret, into every session it reaches. +5. What the module kept in its own file before is judged again when it renders. +6. Removing stays: unregistering a tool server, a hook or settings takes away what runs. +7. A new read-only tool, `claude_code_registrations`, lists every registration with its `by`, `at`, and whether it is still rendered and why not, beside what the terminal set for the node. `by` is the node whose tool runner served the call. The caller was never recorded, because the runner is not told. A tool server's registration kept no `by`. @@ -130,7 +151,9 @@ Each of these must be refused, saying why, and change nothing: - `/claude-code.claude_code_settings_set {settings: {"model": "x"}}`, and with `scope: node, nodes: all`; - `/claude-code.claude_code_permission_add {list: allow, rule: Bash}`; - `/claude-code.claude_code_mcp_register {name: x, url: "https://example.org/mcp", nodes: all}`; -- `/claude-code.claude_code_skill_register {name: x, content: "---\nname: x\nallowed-tools: Bash\n---\nx"}`; +- `/claude-code.claude_code_skill_register {name: x, content: "---\nname: x\nallowed-tools: Bash\n---\nx"}`, and + with the front matter as a flow mapping, and with `@~/.ssh/id_ed25519` in its body; +- `mesh-controller.settings {module: keycloak, node: , values: "{\"url\": \"http://192.0.2.1\"}"}`; - `mesh-controller.settings {module: claude-code, values: "{\"managed_settings\": {\"model\": \"x\"}}"}`, and with `node`, and `clear: "true"`; - `mesh-controller.command {command: "settings set claude-code '{\"mcp_servers\":{}}' --node "}`. From 8a6f19e45727f654f9e5bab2d8e6e2b1dc0d9191 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 09:29:39 +0200 Subject: [PATCH 3/6] Issue 340: record the operator's decisions, and open 342 for passwords taken as settings Shared agent text is the terminal's until a surface of the operator's own updates it, the wider controller rule stands, and a password is not a setting: 342 names every module that takes one through a setting. --- .../00-report.md | 21 +++++-- .../01-diagnosis.md | 36 +++++++++--- .../00-report.md | 57 +++++++++++++++++++ 3 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 04-ISSUES/342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md index 5bcc36bf..8ea5ff78 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md @@ -66,12 +66,19 @@ own settings file. It does not see a tool call that changes the managed files of - no node's layer of the module's settings set `managed_settings` or `mcp_servers`, and no layer had ever been replaced. -## What stays open +## The operator's decisions (2026-10-09) -Text a session reads (instruction sections, output styles, and skills, commands and subagents that run -nothing themselves) is still registered through a verb at the mesh and node scopes. It can ask the operator's -agent on every node to run something, which the session then asks about. The diagnosis says what that risk is. -The operator is deciding whether it becomes terminal-only too. +1. **The wider controller rule stands.** Every module with a mergeable file that does not say + `"trusted": false` takes its settings at the controller's terminal alone. +2. **Shared agent text is the terminal's, for now.** Instruction sections, output styles, skills, commands and + subagents for every node or some are set at the controller's terminal, in the agent module's settings, and + refused through the tools. A person's own home stays free. In the operator's words: "once we build a UI, we + can also update the context through it". A surface of the operator's own, which can tell the operator from + an agent, is the intended way back. +3. **Passwords are not settings.** They come from the key vault and can be rotated. Every module found taking + a password, token or API key through a setting is recorded in + [issue 342](../342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md), to move to + vault-held secrets, not to stay terminal-only settings. ## Ordering @@ -101,7 +108,9 @@ at the controller's terminal. An agent's attempt through any mesh tool is refuse settings file holds. - **In the controller's tests, through the real verb dispatch.** Setting, changing, dropping or clearing the module's `managed_settings`, `mcp_servers` or `role` through the `settings` verb, for one machine or - the mesh, and through the `command` verb, is refused with an error naming this issue. So is a new `url` on a + the mesh, and through the `command` verb, is refused with an error naming this issue. Text for every node or + some, of each kind, is refused through the tools and the import tool, and lands when set in the module's + settings at the terminal; the home scope is still registered. So is a new `url` on a module whose mergeable runtime configuration starts empty, while a file marked `"trusted": false` stays the verb's. The layer is unchanged after every refusal. At the terminal the same is taken. These tests fail on the commit before diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md index 6d8403d1..e44ae0b8 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md @@ -54,10 +54,14 @@ the mesh's guard. But a session in a mode that runs without asking, or a person mesh's own instruction, would run it as the operator. That is the same sideways path as a hook, made one approval longer. -What closing it costs: the mesh's shared instructions and skills, which agents keep up today, would be -registered at the controller's terminal alone. It also does not stop text reaching an agent by other routes -(a file in a repository, a tool's answer). On 2026-10-09 the operator is deciding whether this text becomes -terminal-only too. This issue records the answer when it is given. +What closing it costs: the mesh's shared instructions and skills, which agents keep up today, are set at the +controller's terminal alone. It also does not stop text reaching an agent by other routes (a file in a +repository, a tool's answer). + +**The operator decided on 2026-10-09: this text is the terminal's, for now.** At the mesh and node scopes every +kind is refused through the tools and set at the controller's terminal. The home scope, one account's own +directory, stays registrable. In the operator's words: "once we build a UI, we can also update the context +through it". A surface of the operator's own, which knows who is using it, is the intended way back. ### The routes that write the same files @@ -91,7 +95,9 @@ terminal-only too. This issue records the answer when it is given. This reaches every module with a mergeable file, not only the agent's. On 2026-10-09 that was 25 modules in the catalogue and 11 in the media catalogue, every one with a file that says nothing about trust. Through a verb their settings were any caller's before. Many decide what a consumer trusts or reaches: a provider's address, a -route, a chat id that verifies a sender, a password. From now on the operator sets them at the terminal. An +route, a chat id that verifies a sender. Some take a password, token or API key; the operator ruled on +2026-10-09 that a password is not a setting, and those modules are +[issue 342](../342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md). From now on the operator sets them at the terminal. An author gives a verb back a file nothing trusts with `"trusted": false`. That waits until the node-engine accepts the field, as issue 339 says. `module check`'s count of files that do not say whether they are trusted rose from 9 to 22 over the catalogue. It counts the files whose content names keys. A file that starts empty is not @@ -119,13 +125,29 @@ counted, but is refused all the same. whether it is still rendered and why not, beside what the terminal set for the node. `by` is the node whose tool runner served the call. The caller was never recorded, because the runner is not told. A tool server's registration kept no `by`. +8. **Shared text is set at the terminal** (the operator's decision, above). Through the tools, an instruction + section, output style, skill, command or subagent at the mesh or node scope is refused, saying where it is set, + and the import tool is refused. The module's settings gain `instructions`, `output_styles`, `skills`, + `commands` and `agents`. A setting holds no line break (issue 339), so each text is a list of its lines. + They land where a registration did, laid over what was registered before. What the operator writes there is + not judged for what it asks of a session, because it is the operator's word. An item that is not well formed + is left out, and the render says so. Text registered before is still rendered, and may be unregistered. +9. **Tool servers from the terminal alone** (a separate pull request, to be merged only after the laptop's tool + server is set in that node's `mcp_servers` at the terminal). The managed tool-server file then holds the + mesh's own entry and the `mcp_servers` setting alone. A server left on the bus is listed and may be + unregistered, and no longer starts in a session. That closes, for tool servers, the route by which a holder of + a tool runner's credential could write the bus directly. Text registered before still renders from the bus. + The same move for it is the step left: the three sections set at the terminal, then text no longer rendered + from the bus. ### Existing registrations Read on 2026-10-09 through the module's list tools and the settings verb, on all four nodes: -- three instruction sections for the mesh, registered through the laptop: still rendered; -- one tool server for the laptop alone, a program in the operator's own home: still rendered; +- three instruction sections for the mesh, registered through the laptop: still rendered, until they are set at + the terminal; +- one tool server for the laptop alone, a program in the operator's own home: still rendered, until item 9 + merges, which waits for the operator to set it in the laptop's `mcp_servers` at the terminal; - no hook, no settings, no permission rule; - no layer of the module's settings sets `managed_settings` or `mcp_servers` on any node, and none was ever replaced. diff --git a/04-ISSUES/342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md b/04-ISSUES/342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md new file mode 100644 index 00000000..d8df5551 --- /dev/null +++ b/04-ISSUES/342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md @@ -0,0 +1,57 @@ +--- +status: open +opened: 2026-10-09 +located-in: [] +fixed-by: +amended-design: +--- + +# 342. A password is taken as a setting, and outranks the vault's + +## Symptom + +Reading the catalogue for [issue 340](../340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md) +on 2026-10-09 found modules that take a password, a token or an API key through a **setting**. Nothing was run +against a node, and no node's layers were read for such values, so that no secret is copied into a session. + +The operator's rule, said on 2026-10-09: **a password is not a setting.** It comes from the key vault, and it can +be rotated there. A setting is neither: it is kept in the controller's layers, shown by the settings verb to +whoever reads them, kept in the layer history, and rotated by nobody. + +Two shapes, found by reading the manifests and the modules' clients: + +1. **A mergeable file that holds the vault's secret, which a setting overrides.** The file's content names the + secret as `${secret:…}`, and a layer setting the same key replaces it in the file: + - `baserow` (`password`), `grafana` (`password`), `letta` (`password`), `unifi` (`password`); + - `searxng` (`server.secret_key`); + - the media library's sorter, `kometa` (`plex.token`, `tmdb.apikey`). +2. **A client that reads a secret from its settings file before the vault's.** The settings file starts empty, and + the client reads `cfg. ?? `, so a setting wins over the vault: + - `gitea` (`token`), `home-assistant` (`token`), `icecast` (`password`), `influxdb` (`token`), + `mailu` (`apiKey`), `nextcloud` (`password`), `nodered` (`token`); + - in the media catalogue: `bazarr`, `jackett`, `ombi` and `tautulli` (`apiKey`), `nzbget` and `qbittorrent` + (`password`). + +Not affected, as read: `jira`, `confluence` and `gitlab` read their token from their own secret's file alone. The +watcher and the messenger take a token *file* path, not a token. The four media download steps read the API key +from the application's own configuration, and the download clients' credentials from the provisions. + +## Why it is an issue + +- A secret in a setting cannot be rotated by the vault: the setting outranks the rotated value until someone + removes it by hand. +- It is read back by the settings verb and kept in the layers' history. +- Until issue 340's controller fix, any caller of the settings verb could set one, and so point a module at a + credential of the caller's choice. Since that fix these keys are the terminal's. That keeps agents out, and it + still leaves the secret a setting. + +## Acceptance test (the operator's view) + +No module takes a password, token or API key from a setting. Each comes from a vault-held secret that rotates, +and a setting naming one is refused with the reason. + +## How it will be checked + +To be written with the fix: per module, a test that a setting naming the secret's key is refused (or ignored, and +said), and the catalogue's check refusing a mergeable file whose own keys hold `${secret:…}` that a layer could +replace. From 523bb9c55a74fb5fba9e2ad5d62339d2460a75f4 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 09:43:03 +0200 Subject: [PATCH 4/6] Issue 340: the home scope reached the operator's own home, and bus text still renders The confirmation review found the home scope wrote the operator's own ~/.claude on nodes where the agent is the operator, and that text put straight on the bus at the mesh or node scope still renders; record both, the fix for the first, and what closes the second. --- .../00-report.md | 16 +++++++-- .../01-diagnosis.md | 36 ++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md index 8ea5ff78..89beb87e 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md @@ -72,7 +72,8 @@ own settings file. It does not see a tool call that changes the managed files of `"trusted": false` takes its settings at the controller's terminal alone. 2. **Shared agent text is the terminal's, for now.** Instruction sections, output styles, skills, commands and subagents for every node or some are set at the controller's terminal, in the agent module's settings, and - refused through the tools. A person's own home stays free. In the operator's words: "once we build a UI, we + refused through the tools. A person's own home stays free where it is an agent account's own; the + operator's own home is not written from the tools (see "How it is checked"). In the operator's words: "once we build a UI, we can also update the context through it". A surface of the operator's own, which can tell the operator from an agent, is the intended way back. 3. **Passwords are not settings.** They come from the key vault and can be rotated. Every module found taking @@ -80,6 +81,15 @@ own settings file. It does not see a tool call that changes the managed files of [issue 342](../342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md), to move to vault-held secrets, not to stay terminal-only settings. +## Still open after these pull requests + +- Text at the mesh or node scope written straight into the bus state still renders. It needs a tool runner's + credential. It closes once the three instruction sections are set at the terminal and text is no longer + rendered from the bus. +- The tool servers left on the bus stop rendering with their own pull request, after the laptop's tool server + is set at the terminal. +- Passwords taken as settings: [issue 342](../342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md). + ## Ordering **This must land before ADR 0266's agent account is switched on** (hq pull request 225). Until it lands, the @@ -110,7 +120,9 @@ at the controller's terminal. An agent's attempt through any mesh tool is refuse the module's `managed_settings`, `mcp_servers` or `role` through the `settings` verb, for one machine or the mesh, and through the `command` verb, is refused with an error naming this issue. Text for every node or some, of each kind, is refused through the tools and the import tool, and lands when set in the module's - settings at the terminal; the home scope is still registered. So is a new `url` on a + settings at the terminal. The home scope is refused for any node but the one serving the call, and there + wherever the home is the operator's own; it is taken only where the home is an agent account's own, and a + home item on the bus is never placed in the operator's home. So is a new `url` on a module whose mergeable runtime configuration starts empty, while a file marked `"trusted": false` stays the verb's. The layer is unchanged after every refusal. At the terminal the same is taken. These tests fail on the commit before diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md index e44ae0b8..f0828ac3 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md @@ -38,10 +38,11 @@ documentation of skills, subagents, plugins, output styles and memory, read 2026 | output style, instructions, rule file | the plugin, the managed instruction file, a home | text the model reads; no field runs anything | still registered | | a script beside a skill | the plugin, or a home | only when the agent runs it, through the session's own rules and the guard | still registered | -**Text is a lower class, and stays registrable through a verb. This is a risk, and it is the operator's to -take or close.** At the mesh and node scopes, an instruction section, an output style, and a skill, command or -subagent without the fields above are still registered through the module's tools, so any caller of the mesh -MCP server can register them, an agent confined on the control node included. They reach every session on the +**Text is a lower class. As the review first left it, it was still registrable through a verb, which was a +risk for the operator to take or close; the operator closed it (below).** Before that decision, at the mesh and +node scopes, an instruction section, an output style, and a skill, command or subagent without the fields above +were still registered through the module's tools, so any caller of the mesh +MCP server could register them, an agent confined on the control node included. They reach every session on the nodes they name, as text the model reads: - the instruction sections land in the managed instruction file, which every session reads first and treats @@ -60,9 +61,30 @@ repository, a tool's answer). **The operator decided on 2026-10-09: this text is the terminal's, for now.** At the mesh and node scopes every kind is refused through the tools and set at the controller's terminal. The home scope, one account's own -directory, stays registrable. In the operator's words: "once we build a UI, we can also update the context +directory, stays registrable where that account is an agent's own (the next section). In the operator's words: "once we build a UI, we can also update the context through it". A surface of the operator's own, which knows who is using it, is the intended way back. +### The home scope reached the operator's own home + +The confirmation review found the home scope a way round the rest. `scope: home` takes a list of nodes. On the +laptop, the workstation and the home server the agent runs as the operator, so the home there is the operator's +own `~/.claude`, and its `rules/` and `skills/` load in every session the operator starts. An agent on the +control node could reach it two ways: by naming those nodes in `nodes`, or by addressing the other node's own +tool (`/claude-code.`), which serves it there as if the operator had called. + +The fix, in the module: + +- the home scope is taken only for the node whose module serves the call, and only where its home belongs to an + agent account of its own: the agent runs there as an account that is not the operator's (ADR 0266), and the home + the module writes is that account's. Any other node is refused, because whose home it is cannot be judged from + elsewhere; that node's own module judges it; +- a home item on the bus is never placed in the operator's own home, whoever put it there, and what the module + placed there before is taken back unless the person changed it; +- every node a call names is judged before anything is put, and a list naming one refused node is refused whole. + +Until ADR 0266's account is switched on, no node has an agent account of its own, so the home scope is refused +everywhere. Nothing is registered at the home scope today. + ### The routes that write the same files - **The module's tools**: refused (below). @@ -78,6 +100,10 @@ through it". A surface of the operator's own, which knows who is using it, is th through the tool runner's credential. On the control node, under ADR 0266, the agent account cannot read it. On the other nodes an agent runs as the operator already. The module still judges every item it takes from the state, so a hook, settings or an item that runs something is never rendered, whoever put it there. + **A gap stays open here:** the item check (`Problem`) refuses hooks and settings at every scope, but text at the + mesh or node scope is refused only by `Register`, the tools' path. Text put straight into the state at those + scopes still renders. Doing so needs a tool runner's credential, as with the tool servers. What closes it: the + three instruction sections set at the controller's terminal, then text no longer rendered from the bus. - **The `nox-mesh` plugin** is the module's own render. It carries the guard's hook and nothing else that runs. ### The fix From 0badde3a5678a1dbdf3d5ae946b29f6c1a35e3a6 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 09:43:12 +0200 Subject: [PATCH 5/6] Issue 340: say plainly which test refuses the new url --- .../00-report.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md index 89beb87e..7b0a161f 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md @@ -122,9 +122,9 @@ at the controller's terminal. An agent's attempt through any mesh tool is refuse some, of each kind, is refused through the tools and the import tool, and lands when set in the module's settings at the terminal. The home scope is refused for any node but the one serving the call, and there wherever the home is the operator's own; it is taken only where the home is an agent account's own, and a - home item on the bus is never placed in the operator's home. So is a new `url` on a - module whose mergeable runtime configuration starts empty, while a file marked `"trusted": false` stays the - verb's. The layer is + home item on the bus is never placed in the operator's home. Through the settings verb, a new `url` on a + module whose mergeable runtime configuration starts empty is refused too, while a file marked + `"trusted": false` stays the verb's. The layer is unchanged after every refusal. At the terminal the same is taken. These tests fail on the commit before the fix. - **Live, after both are taken:** the calls in the diagnosis's last section are each refused, and From cdf0e71d871626d6e1869e06a3ae628586d92592 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 09:51:57 +0200 Subject: [PATCH 6/6] Issue 340: removing from or restoring to the operator's own home is refused too --- .../01-diagnosis.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md index f0828ac3..c883c468 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md @@ -81,6 +81,8 @@ The fix, in the module: - a home item on the bus is never placed in the operator's own home, whoever put it there, and what the module placed there before is taken back unless the person changed it; - every node a call names is judged before anything is put, and a list naming one refused node is refused whole. +- removing an item of the home, or putting one back (`claude_code_home_remove`, `claude_code_home_restore`), is + refused the same way wherever the home is the operator's own, because an agent could remove a rule that constrains it. Until ADR 0266's account is switched on, no node has an agent account of its own, so the home scope is refused everywhere. Nothing is registered at the home scope today.