claude-code: point the agent at the mesh's tools and guard its shell against ssh to the mesh (hq ADR 0245)

The agent kept running ssh <machine> journalctl while the journal verb existed. The managed
instructions now carry an "instead of" table generated from what each verb says it replaces, and
the plugin carries a PreToolUse guard that refuses ssh to a mesh machine and local work-arounds for a
mesh name, naming the tool or saying one must be created. The operator's override is read from the
session's start environment and recorded.
This commit is contained in:
jochen
2026-10-07 20:27:31 +02:00
parent ba6058abb9
commit 24a3fac2ba
13 changed files with 2033 additions and 14 deletions
+34 -3
View File
@@ -23,8 +23,8 @@ whenever the node's tool runtime collects the module's tools:
|---|---|
| `managed-mcp.json` | the tool servers every session loads: the mesh's console as `mesh`, and the servers set in this module's `mcp_servers` setting. **Exclusive**: a server not listed here does not load — not one added with `claude mcp add`, not a project's `.mcp.json`, not a plugin's |
| `managed-settings.json` | the keys set in this module's `managed_settings` setting, then the settings registered through this module (the mesh's, then this node's), under the mesh's own keys: the repositories' attribution convention, the claude.ai connectors kept beside the managed servers, the key-helper while the node holds an API-key licence, and the two that name the `nox-mesh` marketplace and enable its plugin |
| `CLAUDE.md` | how a session on this mesh works, this node's name and role, the conventions — then the instruction sections registered for every node and for this one |
| `marketplace/` | the `nox-mesh` plugin (hq ADR 0216): the skills, subagents, commands, hooks and output styles registered for every node and for this one, offered in a session as `nox-mesh:<name>`. Replaced whole, staged beside and swapped in |
| `CLAUDE.md` | how a session on this mesh works, this node's name and role, the conventions, the **"instead of" table** — then the instruction sections registered for every node and for this one |
| `marketplace/` | the `nox-mesh` plugin (hq ADR 0216): the skills, subagents, commands, hooks and output styles registered for every node and for this one, offered in a session as `nox-mesh:<name>` — and the **guard on the agent's shell** (`guard/`), the mesh's own hook, first. Replaced whole, staged beside and swapped in |
Under the operator's home: `~/.claude/.credentials.json`, only when the licence manager hands this node a
subscription token; and what is registered at the **home** scope for this node — a skill, subagent,
@@ -49,9 +49,40 @@ must see, a node that joins later included — kept, so it carries no secret eit
| an MCP server registered through this module | a key in the module's `servers` state — `all.<server>` for every node, `<node>.<server>` for one; every node watches it and renders what applies to it, a node's own entry over the one for every node. A node that joins later, or was off, reads the whole current set at start; unregistering is a delete. An entry with a secret in its `env` or `headers` is refused by the runtime |
| the agent's configuration (hq ADR 0216) | a key in the module's `config` state per registration — `mesh.<kind>.<name>` for every node, `node.<node>.<kind>.<name>` for one, `home.<node>.<kind>.<name>` for one account's own directory — the item and its files in one value, at most 256 KiB. Every node watches it and renders what applies to it, a node item over a mesh item of the same kind and name |
## The mesh's tools first (hq ADR 0244)
The agent kept reaching for `ssh <machine> journalctl` while the service manager's `journal` verb existed. So:
- **The "instead of" table.** Every seat verb and module tool may say which shell commands it replaces
(`replaces`, in the seat's definition or the module's manifest). The module asks the controller — `tools`,
`modules`, `nodes`, `node` — at start, every ten minutes and on `claude_code_render`, keeps the answer in its
state (`mesh-tools.json`), and renders it into `CLAUDE.md` as one row per seat or module: `<node>/<seat>.` and
each verb with the commands it replaces. Generated, never written by hand: a verb that gains `replaces` is in
the next render. Ordered by what the guard here refused most, then the machines' seats, the modules, the
mesh's seats; at most sixteen rows, the rest one `mesh_search` away.
- **The guard.** A PreToolUse hook on `Bash`, `Edit`, `Write`, `MultiEdit` and `NotebookEdit`: this module's own
binary (`claude-code guard`), copied root's into the plugin with what it judges with (`guard.json`: the
machines by name, domain and address, and the replaced commands), run by its path under `/etc/claude-code` so
the session cannot change it. It refuses `ssh`, `scp`, `sftp`, `rsync`, `mosh` and `autossh` to a mesh machine
(any `*.internal` name, a machine's name or a name under its domains, one of its addresses, as `ssh -G` reads
the destination; a jump through one too), writing `/etc/hosts` or `/etc/resolv.conf`, and `HOSTALIASES` —
naming the verb that does the job on that machine when one says it replaces the command, and otherwise that a
missing tool is created in the module that owns it, never worked around. **Stated, not silent:** an ssh login
as `git` is the forge's account, which runs nothing but git, and passes; a git remote is never an ssh command
line anyway.
- **The operator's override**: `MESH_GUARD_OVERRIDE=<why>`, exported in the operator's own shell before the
session starts. It is read from the session's environment as the kernel kept it at exec
(`/proc/<pid>/environ` of the agent's process), so nothing a session does — a command's `export`, a
settings `env` key — can set it, and a command naming it is refused outright. Every override and every
refusal is a line in the module's `guard.log`; an override that cannot be recorded is not honoured.
`claude_code_guard` shows the rules, the data and the record.
It is a guard against the habit, not a sandbox: a command written to hide what it runs can hide it, and the
record is how a habit that found a way round is seen.
## Tools
`claude_code_status`, `claude_code_render`, `claude_code_pull`, `claude_code_grant` (for the licence
`claude_code_status`, `claude_code_render`, `claude_code_guard`, `claude_code_pull`, `claude_code_grant` (for the licence
manager), `claude_code_mcp_list`,
`claude_code_mcp_register` (this node by default; `nodes: "all"` or a list for more — called for this
node alone, its answer names the other nodes running claude-code), `claude_code_mcp_unregister`.