Files
mesh-catalog/modules/claude-code
jochen f72a435487
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
claude-code: read where it runs from the controller's JSON answer
nodesRunningMe looked for printed lines in what is a JSON list, so every register tool's "also on"
hint and every nodes: "all" named no machine.
2026-10-07 20:28:06 +02:00
..
2026-10-04 12:27:36 +02:00
2026-10-04 12:27:36 +02:00

claude-code

The operator's agent on a machine (novox/hq design 36): its package, its machine-wide managed configuration, and the consumer side of the Anthropic licence manager (design 39, ADR 0183).

What it owns

Two directories, declared, so the mesh refuses a second module owning either:

  • /etc/claude-code, the agent's machine-wide managed directory, root's, 0755.
  • ~/.claude under the operator account's home, the operator's, 0700. The module owns the directory — that it exists, who owns it, its mode — and of what is inside only what it writes. Everything else in it (memory, history, projects, local settings, a person's own rules and skills) is the person's and is never read or written (hq ADR 0182). Unassigned, the module leaves the directory: the host removes a directory only when it is empty.

What it writes

Under the agent's managed directory, /etc/claude-code, owned whole by this module and rewritten whenever the node's tool runtime collects the module's tools:

file holds
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, 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, command, output style, or instructions as a rule file — each path recorded in the module's state (home-placed.json). It writes, changes and removes only those — never a path the person made, even one with the same content, and never through a directory that is a symbolic link. A placed file changed by hand is left alone, and one the person deleted stays deleted until the item is unregistered (hq ADR 0182). The status tool reads the rest of the home's items to report them; nothing else is read or written.

Over NATS

Everything between this module and the rest of the mesh is NATS, in three kinds: an event says that something happened and carries no secret, because a stream keeps it; a request carries a token, because nothing keeps it (hq design 32 §10); and state is the current value of something every node must see, a node that joins later included — kept, so it carries no secret either (hq ADR 0201).

what how
what this node holds the module's holdings state, one key for this node — the account, the kind, fingerprints and expiries, never a token — written at start and whenever the credentials file changes (hq ADR 0206)
a person ran /login here the credentials file gains a refresh token this module never writes; its next report shows it, and the licence manager asks claude_code_grant for it, giving its key — the one time a refresh token leaves the node, for the manager to adopt by refreshing it
what this node should hold the licence manager's bindings state, this node's key; on a newer generation this module asks anthropic-licence-manager.current for its token, sealed to the key it sends, and writes it access-token-only — so the agent here never refreshes. A node that was off reads its key when it is back
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 0245)

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_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.

The agent's configuration (hq ADR 0216), each registered at a scope — mesh (the default), node (nodes: a list, or "all" for every node running claude-code; absent is this node) or home (the operator account's own ~/.claude on those nodes):

  • for each kind — skill, agent, command, hook, output_style, instructions — claude_code_<kind>_list, _register, _unregister. A skill is its files (files, or content for a lone SKILL.md); a hook is an event, a matcher, a command and its scripts as files, with ${HOOK_DIR} in the command naming their directory. Hooks and settings take no home scope;
  • claude_code_settings_get, _set (merged into the scope, or replace), _clear; claude_code_permission_add and _remove for one allow, ask or deny rule. The agent refuses to loosen its own settings: these are the operator's to call;
  • claude_code_config_list, _show (one registration in full), _status (what applies here, the plugin as written, and the home's own items — which the mesh placed, which share a name with a mesh item, which call a tool server not loaded here) and _import (an item of this node's home, registered at a scope; the original stays);
  • claude_code_home_show (kind, name): one item of this node's home in full — a skill, subagent, command, output style, rule file (instructions), or the account's own memory ~/.claude/CLAUDE.md (memory) — and whether the mesh placed it;
  • claude_code_home_remove (kind, name, why): removes one item the person made, on their word — removing it is the person's act, and this tool is that act made explicit. why is required; what the mesh placed is refused (its _unregister owns it), and so is a symbolic link. A copy is kept first in the module's state, removed-from-home/<date>/<time>-<kind>-<name>/, with a removal.json note, and the removal and its reason are appended to removed-from-home/removed.log; the answer names the copy;
  • claude_code_home_removed: every kept removal, newest first, with its keptAt and whether it was put back;
  • claude_code_home_restore (kept): puts a removal's copy back at its path — refused when something is there now, or when the copy is not, file by file, what its removal.json digests say was removed. Logged to removed.log and the journal; the copy stays, marked with a restored.json.

A new session takes a change; a running one at /reload-plugins.

Settings

Per node or for the whole mesh, through mesh-controller.settings module=claude-code:

  • role — what this node is, in a few words; shown to every session.
  • mcp_servers — extra tool servers, set by the operator for the mesh or a node, beside the ones registered through the tools; keyed by name, in the vendor's .mcp.json entry shape ({"type":"http","url":…} or {"type":"stdio","command":…,"args":[…]}). The name mesh is the module's own and cannot be set. Put a person's own servers here, or they stop loading.
  • managed_settings — keys of the agent's managed settings, in the vendor's settings.json shape: permissions (allow, ask, deny), autoMode (environment, allow, soft_deny), env, hooks and so on. Managed settings outrank every other scope, so a rule here holds in every session on the node. The mesh's own keys (attribution, allowAllClaudeAiMcps, apiKeyHelper) are laid last and cannot be set. A setting layer is replaced whole: setting managed_settings without role or mcp_servers clears those in that layer.

On a machine that carried the predecessor

Remove these by hand, once; the mesh removes nothing it did not make (ADR 0182):

  • ~/.claude/CLAUDE.md
  • ~/.claude/rules/00-hal-mesh.md, ~/.claude/rules/conventions.md
  • ~/.claude/skills/cleanup/, ~/.claude/skills/hal-switch-license/
  • the hand-made console entry in ~/.claude.json under mcpServers — it is ignored now anyway

Escalation

Writing /etc/claude-code needs root. The runtime runs as the operator account, and the module uses that account's passwordless sudo; on a machine without it, claude_code_render says so and nothing is written.

Code

Go, one binary (cmd/claude-code) the node's runtime launches. Tested with go test ./...; the managed instruction file is held to the TypeScript renderer it replaced (testdata/rendered-by-typescript.json), and the sealed box is the licence manager's own format.