Files
mesh-catalog/modules/claude-code/README.md
T
jochen 92f78db970 claude-code: act on the review of the nox-mesh plugin
Refuse paths with empty, dot or parent segments and a file that is also a
directory; take an item only when its key says what it is; write the view
under its lock; expand nodes all and check node names; merge the plugin's
entries into the operator's own; render every file past one that fails;
in the home, never take over the person's file, never write through a
symbolic link, keep a deleted file deleted, keep the kind's directory; and
refuse settings that would deny the console or the marketplace.
2026-10-05 14:29:53 +02:00

113 lines
8.5 KiB
Markdown

# 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 — 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 |
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 |
## Tools
`claude_code_status`, `claude_code_render`, `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).
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.