ADR 0273: act on drift at once, watch settings.local.json and plugins, and move unrendered files after adoption
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

Review of hq #254: a hand edit is written back on the look that finds it, and a key that
decides what runs is never taken over; the guard and the condition cover settings.local.json,
plugins and plugin installs; after a node's first adoption an unrendered file is moved to the
copies; autoMode is a permission rule; the named leftovers, the owned and excluded entries,
pending requests with the exact nox line, and the agent's existing corrections as first lessons.
This commit is contained in:
jochen
2026-10-09 19:24:35 +02:00
parent 2b1edf046d
commit a5433164c5
3 changed files with 222 additions and 106 deletions
@@ -125,31 +125,54 @@ in short:
### 1. The configuration under the agent's home is the module's, whole
**What "completely empty" means.** The operator's words, "it should be completely empty", are read as: the
agent's home holds **nothing a person or an agent put there by hand**. Every configuration file in it is the
module's rendering, and the only other entries are the program's own working state, named below.
**The `claude-code` module owns every configuration file under the agent's home directory** (`~/.claude` of
the account the agent runs as, ADR 0266 §5): the home instruction file, `rules/`, `skills/`, `commands/`,
`agents/`, `hooks/`, and `settings.json`. **Nothing there is written by hand.** Each is rendered by the
module's code from registrations, on every node, whenever a registration changes, and nothing else writes
them. A configuration file the vendor adds later joins the list by name, in the module, before the module
renders anything into it.
`agents/`, `hooks/`, `output-styles/`, `settings.json` and `keybindings.json`. **Nothing there is written by
hand.** Each is rendered by the module's code from registrations, on every node, whenever a registration
changes, and nothing else writes them. A configuration file the vendor adds later joins the list by name, in
the module, before the module renders anything into it.
**The program's working state is excluded by name**, and stays the program's, in ADR 0182's *found* class
or *written by the module's own process*: the credentials file (ADR 0183), the history, the sessions and
their environments, the projects directory with every project's memory, the shell snapshots, the file
history, the debug, cache, paste and telemetry directories, the statistics, the daemon's files, the
program's own plugin records and cache (`plugins/`), the vendor's synced skills (`skills/synced/`), the
backups, worktrees, jobs, feedback, state and IDE directories, and `settings.local.json`, which the program
writes with the permission answers a person gives in a session. The module never reads, writes or removes
any of these. It names the list in its manifest's documentation, and the status tool shows an entry it does
not know under either heading, so a new vendor file is seen rather than guessed about.
or *written by the module's own process*: the credentials file `.credentials.json` (ADR 0183), the history,
the sessions and their environments, the projects directory with every project's memory, `todos/`,
`plans/`, `statsig/`, the shell snapshots, the file history, the debug, cache, paste and telemetry
directories, the statistics, the daemon's files (`daemon/`, its lock, log and status files,
`daemon-auth-status.json`, `daemon-auth-cooldown`), the program's dot-markers (`.last-cleanup`), the
vendor's synced skills (`skills/synced/`), and the backups, worktrees, jobs, feedback, state and IDE
directories. The module never writes or removes any of these, and names the list in its manifest's
documentation.
**Two entries are the program's to write, and watched**, because each decides what a session runs and
nothing else would see a change to it: `settings.local.json`, where the program records the permission
answers a person gives in a session, and `plugins/`, the program's record of its marketplaces and installed
plugins, and their cache. The module never writes them; §4 says what it raises and what the guard refuses.
**What is neither, by name.** Measured on the laptop on 2026-10-09: `.hal-binding.json`, a leftover of the
retired predecessor, and `.credentials.json.testbak`, a copy of the credentials file left by a test. **No
copy of the credentials file** (`.credentials.json` followed by anything) belongs in the home: each is
reported, and removed on the operator's word with ADR 0243's tool, never kept as a copy in the module's
state, because it holds a token. **Every other entry the module knows under neither list is a condition**
(§4), so a new vendor file is a question the operator answers, not a default.
**The keys the program itself writes into `settings.json` on a person's choice** — the model, the spinner,
the drafts and the other preferences the vendor's own `/model` and `/config` commands set — are not hand
edits. The module keeps a short list of them. When the program changes one of those keys, the module takes
the new value over into the operator's registered settings (§2), reports it in the status tool, and the
change follows the operator to every node. That is ADR 0182's *written into* class, applied the other way
round: the file is the module's, and the program is allowed into a few named keys of it. This is the
mechanism that keeps a `/model` choice from being reverted on the next render, the failure ADR 0182's context
records.
edits. The module keeps a short list of them, preferences only. When the program changes one of those keys,
the module takes the new value over into the operator's registered settings (§2), reports it in the status
tool, and the change follows the operator to every node. That is ADR 0182's *written into* class, applied
the other way round: the file is the module's, and the program is allowed into a few named keys of it. This
is the mechanism that keeps a `/model` choice from being reverted, the failure ADR 0182's context records.
**A key that decides what runs is never taken over**: `hooks`, `permissions`, `mcpServers` and the other
tool-server keys, `env`, `apiKeyHelper`, `statusLine`, `autoMode`, and every key not on the list. Changed on
disk, it is written back on the look that finds it (§4).
**`autoMode` is a permission rule.** It tells the program's unattended mode what it may do without asking,
which ADR 0213 calls policy, not preference. It is barred from the operator scope like `permissions` and
`hooks`, and belongs in the managed settings, set at the controller's terminal in the module's
`managed_settings` for the mesh or a node.
### 2. A fourth scope, the operator's, and a new kind, the personal rule
@@ -166,10 +189,12 @@ The kinds at the operator scope:
lesson it was accepted from) and when. The vendor reads both into every session of that account;
- the operator's own **skills, commands and subagents**, under the person's own names, without the plugin's
prefix;
- the operator's **settings** (`settings.json`), as one registered object, laid under the managed settings
as the vendor lays them.
- the operator's **settings** (`settings.json`), as one registered object of the preference keys of §1,
laid under the managed settings as the vendor lays them; and the operator's **keybindings** and **output
styles**.
**Never at the operator scope: a hook, a permission rule, a tool server, or an item that runs anything.**
**Never at the operator scope: a hook, a permission rule (`autoMode` included), a tool server, an
environment or key-helper setting, or an item that runs anything.**
Those stay the terminal's, in the managed settings and the managed tool-server file (issue 340). A personal
rule, skill, command or subagent that holds a shell line or the front matter issue 340 refuses is refused at
the operator scope too.
@@ -177,8 +202,10 @@ the operator scope too.
**The operator's existing items are imported as the first registrations**, through the import tool ADR 0216
§6 named, at the operator scope: the two rule files on the laptop become the first two personal rules; the
laptop's skills and the workstation's subagent are imported or removed, each on the operator's word
(ADR 0243); `settings.json` on the laptop becomes the operator's registered settings; the retired `cleanup`
skill and the three instruction files are removed with a copy kept. Until each item is imported or removed it
(ADR 0243); the preference keys of `settings.json` on the laptop become the operator's registered settings,
and its `autoMode` key moves to the managed settings at the terminal, because it is a permission rule (§1);
the two leftovers §1 names are removed on the operator's word; the retired `cleanup` skill and the three
instruction files are removed with a copy kept. Until each item is imported or removed it
is a condition (§4).
### 3. Who may register into the operator's home, and issue 340's protection stands
@@ -196,6 +223,14 @@ taken **only** by one of two routes, and refused through any other:
for that ask, recording it. Until a channel that proves its sender answers approve for the module, the
ask is refused at once with the reason and names the terminal's line.
**The module's tools stay the operator's way in.** A tool called for the operator's home, while no channel
answers approve, does not only refuse: it records the request as **pending** in the module's state (what,
at which scope, asked when, from which node) and answers with the exact line to type: `nox settings set
claude-code '<json>'`, the request merged into the module's current layer, so that typing it changes nothing
else. The status tool lists pending requests; one is dropped when the terminal's layer holds it, or after
7 days. An agent asking for a rule therefore hands the operator one line to check and type, never a
description to translate.
The home scope into **an agent account's own home** stays as issue 340 left it: that node's own tools take
it, because that home is no person's. A home-scope item never shadows a personal rule: the module renders
the operator's items first and refuses a home-scope name an operator-scope item already uses.
@@ -206,29 +241,49 @@ operator's own account on a node where no agent runs as it.
### 4. A hand edit is a condition, and the guard refuses it
**On every look**, and within a minute of a change on disk, the module compares the configuration paths of
§1 with what it last rendered. Any difference is a **finding**, said as the module's own health (ADR 0240)
on that node, and the controller raises the condition **`agent-home-drift`** for it, a warning that needs the
operator, naming each path and what differs:
**On every look**, at least once a minute and at once on a change on disk, the module compares the
configuration paths of §1 with what it last rendered, and reads the two watched entries. Any difference is a
**finding**, said as the module's own health (ADR 0240) on that node, and the controller raises the
condition **`agent-home-drift`** for it, a warning that needs the operator, naming each path and what
differs. **The look that finds a difference acts on it**; nothing waits for the next render:
- **a file nobody rendered** (a hand-written rule, a copied skill, a file left by a retired generator): left
in place and reported, never removed by the module. The operator's answers are the import tool (register
it at a scope) or the removal tool (ADR 0243: a reason, a copy kept). A file the operator neither imports
nor removes keeps the condition open;
- **a rendered file changed by hand**: written back at the next render, the hand version kept as a copy in
the module's state and shown by the status tool, so the operator's words are not lost — the mesh owns the
truth, and the way to change the file is to change the registration;
- **a rendered file missing**: rendered again, and reported once.
- **a rendered file changed by hand**, `settings.json` above all: written back on that look, the hand version
kept as a copy in the module's state (`home-copies`) and shown by the status tool, so the words are not
lost. The one exception is a preference key of §1 the program changed, which is taken over. A `hooks`,
`permissions`, tool-server or other key that decides what runs, added or changed in `settings.json`, is
**always written back**, never taken over;
- **a rendered file missing**: rendered again on that look, and reported once;
- **a file nobody rendered** under `rules/`, `skills/`, `commands/`, `agents/`, `hooks/` or
`output-styles/`:
- **before the node's first adoption** (the imports of §2, done once per node by the operator, and recorded
in the module's state): left in place and reported, never removed by the module. The operator's answers
are the import tool (register it at a scope) or the removal tool (ADR 0243: a reason, a copy kept);
- **after it**: **moved** on that look into `home-copies`, kept and restorable with ADR 0243's restore tool,
and raised. A session never loads a file nobody registered for longer than one look. Importing it from
the copy is the operator's way to keep it;
- **an entry the module knows under neither list of §1**: reported and raised, never touched; the operator
removes it on their word or the module's next build names it;
- **`settings.local.json` holding any key other than `permissions`**: raised, naming the key. The program
writes only that key there, so another is somebody's hand;
- **`plugins/` naming a marketplace or an installed plugin other than the vendor's own marketplace and the
mesh's `nox-mesh`**: raised, naming it. A plugin carries hooks, so one installed by hand runs in every
session.
The condition clears on the first look that finds no difference. It is never urgent, and the first-node gate
never reads it as a fault of a build (as
[ADR 0254](0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md)
treats a wait for a person).
**The guard on the agent's shell** (ADR 0245) refuses, before it runs, any shell command or file edit whose
target is under a configuration path of §1 — a write, an append, a copy or move into it, an in-place edit,
a removal — naming the module's tool that does the job, and saying that a rule the agent wants is a lesson
(§5). The operator's override of ADR 0245 §5 applies and is recorded. The guard does not watch the
**The guard on the agent's shell** (ADR 0245) refuses, before it runs, naming the module's tool that does the
job and saying that a rule the agent wants is a lesson (§5):
- any shell command or file edit whose target is under a configuration path of §1 — a write, an append, a
copy or move into it, an in-place edit, a removal;
- the same aimed at `settings.local.json` or at a `.json` file under `plugins/`;
- `claude plugin install`, `claude plugin marketplace add` and the in-session `/plugin install` and
`/plugin marketplace add`, whatever the plugin.
The operator's override of ADR 0245 §5 applies and is recorded. The guard does not watch the rest of the
working state.
### 5. Lessons: an agent proposes, the operator decides, and the agent is reminded
@@ -248,6 +303,15 @@ the trigger, the cause and the rule, in the same turn, before going on. It says
in one line. An agent that is unsure whether a correction is a lesson records it; the operator declines what
is not.
**A correction goes to the register, not to the agent's own memory.** The program's per-project memory
(§1, working state) stays the agent's notebook for facts about a project; a way of working the operator
corrected is a lesson, so that it reaches every node and the operator sees it.
**The first lessons are the agent's existing corrections.** Once the register exists on a node, the agent
there proposes, unasked, every note in its own memory that records a correction the operator gave (at least 9
on the laptop on 2026-10-09), each as a lesson with the note's trigger, cause and rule, and says so in one
line. The operator accepts or declines them like any other.
**A proposed lesson is not a rule.** It is rendered nowhere and reaches no session as an instruction. The
module shows it to the operator for a yes: an approve ask (§3 route 2) whose words carry the trigger, the
cause and the rule, with Accept and Decline as its options; or, until a channel answers approve, the list
@@ -294,7 +358,7 @@ Recording a proposal needs no yes: it writes the register and nothing a session
- The operator's rules follow the operator: a rule written once reaches the agent's home on every node
within a minute, and a session on any node starts with it.
- A hand edit under `~/.claude` comes back at the next render, with the hand version kept, and is said as a
- A hand edit under `~/.claude` is undone within a minute, with the hand version kept, and is said as a
condition: no silent difference, and no lost words.
- The retired skill and the three stale instruction files, found on four nodes since 2026-10-02, are
removed on the operator's word with copies kept, and nothing like them can stay unnoticed again.
@@ -317,16 +381,20 @@ Recording a proposal needs no yes: it writes the register and nothing a session
| Rule | Checked by |
|---|---|
| Every configuration path of §1 is rendered from registrations and nothing else; the working state is never read, written or removed | the module's render test over a seeded home: the configuration files match the registrations byte for byte; every excluded entry is byte-identical before and after; an unknown entry is reported |
| A program-written key of `settings.json` is taken over, not reverted | the module's test: the model key changed on disk appears in the operator's registered settings on the next look and in the rendered file on every node; a key outside the list changed on disk is drift |
| Every configuration path of §1 is rendered from registrations and nothing else; the working state is never written or removed; an entry under neither list is a condition | the module's render test over a seeded home: the configuration files (`keybindings.json` and `output-styles/` included) match the registrations byte for byte; every excluded entry (`todos/`, `plans/`, `statsig/`, `.last-cleanup`, `daemon-auth-cooldown` among them) is byte-identical before and after; an unknown entry, `.hal-binding.json` and a `.credentials.json.*` copy each raise `agent-home-drift` and are left in place; removing a credentials copy keeps no copy of it |
| A program-written preference key of `settings.json` is taken over; a key that decides what runs is always written back; `autoMode` is never taken at the operator scope | the module's test: the model key changed on disk appears in the operator's registered settings on the next look and in the rendered file on every node; `hooks`, `permissions`, `mcpServers`, `env`, `apiKeyHelper`, `statusLine` and `autoMode` added or changed on disk are each written back on the look that finds it and never reach the registration; `autoMode` in a `personal_settings` value is refused |
| A personal rule renders into the agent's home on every node naming an operator account, under `rules/` and in the home instruction file | the module's render test for one machine of three: the operator's own home on two, the agent account's home on the one naming one; live: a rule set at the terminal is in a new session's context on each node |
| Nothing that runs is taken at the operator scope | the module's tests: a hook, a permission rule, a tool server, and a skill, command or subagent holding a shell line or the refused front matter are each refused at the operator scope with issue 340's reason |
| A registration into an operator's home is taken only at the terminal or on a warrant | the module's tests through its real tool dispatch: each register, import and remove tool at the operator scope, or for an operator's home, publishes an ask and writes nothing; the same with a warrant for that ask acts once; a warrant for another ask, or a replayed one, is refused; while no channel answers approve the ask is refused at once naming the terminal's line. The controller's test: the module's layer is the terminal's (issue 340's) |
| A hand edit is a condition, and nothing found is removed by the module | the module's tests: a file nobody rendered is reported and left; a rendered file changed by hand is written back, its hand version kept and shown; a missing one is rendered again; the condition clears on a clean look; the controller's test that `agent-home-drift` is a warning the first-node gate never reads as a fault |
| The guard refuses a write under a configuration path and lets the working state alone | the module's guard tests: a redirect, append, copy, move, in-place edit and removal under each path refused naming the tool; the same under `projects/` passes; the operator's override honoured and recorded |
| A hand edit is a condition, undone on the look that finds it | the module's tests with a clock: a rendered file changed by hand (`settings.json` among them) is written back on the same look, within a minute, its hand version kept and shown; a missing one is rendered again on that look; the condition clears on a clean look; the controller's test that `agent-home-drift` is a warning the first-node gate never reads as a fault |
| Before a node's first adoption a file nobody rendered is left; after it, it is moved to the copies and raised | the module's tests: an unrendered file under each of `rules/`, `skills/`, `commands/`, `agents/`, `hooks/`, `output-styles/` is reported and left before the adoption is recorded; after it, the same file is gone from the home on that look, present in `home-copies`, restorable with the restore tool, and the condition names it |
| `settings.local.json` and `plugins/` are watched | the module's tests: a key other than `permissions` in `settings.local.json` raises the condition naming it; a marketplace or installed plugin other than the vendor's and `nox-mesh` in `plugins/` raises it naming it; neither file is written by the module |
| The guard refuses a write under a configuration path, to `settings.local.json` or `plugins/*.json`, and a plugin install, and lets the working state alone | the module's guard tests: a redirect, append, copy, move, in-place edit and removal under each path, and aimed at `settings.local.json` and a `.json` under `plugins/`, refused naming the tool; `claude plugin install`, `claude plugin marketplace add` and their in-session forms refused; the same write under `projects/` passes; the operator's override honoured and recorded |
| A home-scope name may not shadow a personal rule | the module's test: with a personal rule, skill, command or subagent registered at the operator scope, a home-scope registration of the same name and kind is refused naming the operator-scope item, and the render holds the operator's item |
| A tool called for the operator's home leaves a pending request and the exact terminal line | the module's tests: the call writes nothing into the home, records one pending request, and answers a `nox settings set claude-code '…'` line which, applied to the current layer, adds that item and changes no other key; the request is dropped once the layer holds it, and after 7 days |
| A lesson is recorded with its trigger, cause and rule; a proposal renders nowhere; an accepted one is a personal rule; a declined one is not proposed again | the module's tests over the register: register, list, accept, decline, expiry; the render test that only accepted lessons reach `rules/`; a second proposal matching a declined rule refused with the reason |
| The agent is reminded at session start and before a matching tool | the module's hook tests: the session-start hook prints the recent and matching rules; the guard prints a matching lesson's rule once per session and refuses nothing for it |
| An agent records a lesson when corrected | the mesh's instruction section says so (`working-on-the-mesh`), checked by reading it on each render; live: a correction given in a session is in the register within that turn, and the ask reaches the operator |
| An agent records a lesson when corrected, and proposes its existing corrections first | the module's render test: the rendered managed instruction file's `working-on-the-mesh` section holds the paragraph on when a lesson is recorded, that a correction goes to the register and not to the agent's memory, and that existing corrections are proposed first, and the test fails when the paragraph is taken out of the section; live: a correction given in a session is in the register within that turn, and the laptop's memory corrections are in the register as proposals after WP9 |
| Live, at acceptance (the operator's view) | on the laptop: `~/.claude` holds the rendered configuration and the program's working state and nothing else; a rule written by hand into `rules/` raises `agent-home-drift` within a minute and is listed by the status tool; `nox settings set claude-code '{"personal_rules": …}'` on the control-node puts the rule into a new session on every node; a correction given in a session appears in the lessons list, is accepted at the terminal, and the next session on another node names it at start |
## References
@@ -395,54 +395,72 @@ the helper of §9 where that is an agent account):
| `rules/<name>.md` | one personal rule each |
| `skills/<name>/`, `commands/<name>.md`, `agents/<name>.md` | the operator's own skills, commands and subagents at the operator scope, under their own names; and the home-scope items where the home is an agent account's own |
| `hooks/` | nothing: a hook is the terminal's, in the managed settings. The directory is kept empty |
| `settings.json` | the operator's registered settings, with the keys the program itself writes on a person's choice (the model, the spinner, the drafts, the other preferences the vendor's own commands set) taken over into the registration when the program changes them, so a choice made in one session follows the operator |
| `output-styles/<name>.md`, `keybindings.json` | the operator's output styles and keybindings at the operator scope |
| `settings.json` | the operator's registered settings, preference keys only, with the keys the program itself writes on a person's choice (the model, the spinner, the drafts, the other preferences the vendor's own commands set) taken over into the registration when the program changes them, so a choice made in one session follows the operator. A key that decides what runs (`hooks`, `permissions`, `mcpServers`, `env`, `apiKeyHelper`, `statusLine`, `autoMode`, any key not on the list) is never taken over: changed on disk, it is written back on the look that finds it. `autoMode` is a permission rule, so it lives in the managed settings at the terminal |
Everything else under the directory is the program's working state, excluded by name in the module's
documentation and never read, written or removed: the credentials file (§5), the history, the sessions and
their environments, the projects and their memory, the shell snapshots, the file history, the debug, cache,
paste and telemetry directories, the statistics, the daemon's files, `plugins/`, `skills/synced/`, the
backups, worktrees, jobs, feedback, state and IDE directories, and `settings.local.json`. An entry the module
knows under neither heading is reported by the status tool, so a vendor's new file is a question, not a
default.
"Completely empty", the operator's words, means: nothing in the home was put there by hand. What is not the
module's rendering is the program's own working state, excluded by name in the module's documentation and
never written or removed: the credentials file (§5), the history, the sessions and their environments, the
projects and their memory, `todos/`, `plans/`, `statsig/`, the shell snapshots, the file history, the debug,
cache, paste and telemetry directories, the statistics, the daemon's files (`daemon-auth-cooldown` among
them), the dot-markers (`.last-cleanup`), `skills/synced/`, and the backups, worktrees, jobs, feedback, state
and IDE directories. **Two are the program's and watched**: `settings.local.json` (the permission answers a
person gives) and `plugins/` (marketplaces and installed plugins). **Named as neither**: `.hal-binding.json`,
a predecessor's leftover, and every copy of the credentials file (`.credentials.json` followed by anything,
such as `.credentials.json.testbak`): reported, and removed on the operator's word, no copy kept because it
holds a token. **Any other entry is a condition** until the operator removes it or the module names it.
**Who may register into the operator's home.** A registration at the operator scope, or at the home scope
into a home that is the operator's own, is taken by two routes and refused through any other:
1. **at the controller's terminal** ([ADR 0272](../../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)):
the module's settings `personal_rules`, `personal_skills`, `personal_commands`, `personal_agents`,
`personal_settings`, `lessons_accepted` and `lessons_declined`, set with `nox settings set claude-code …`.
The module's whole layer is already the terminal's (issue 340);
`personal_output_styles`, `personal_keybindings`, `personal_settings`, `lessons_accepted` and
`lessons_declined`, set with `nox settings set claude-code …`. The module's whole layer is already the
terminal's (issue 340);
2. **on a warrant** ([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)):
the module uses the `operator-channel` seat as an asker. A register, import or remove tool called for the
operator's home acts on nothing: it publishes an approve ask showing the item in full, its digest binding
the text, name, kind and scope, and acts once on the warrant for that ask. While no channel answers approve
for the module, the ask is refused at once with the reason, naming the terminal's line.
the text, name, kind and scope, and acts once on the warrant for that ask.
Nothing that runs — a hook, a permission rule, a tool server, a skill or command holding a shell line or the
front matter issue 340 refuses — is taken at the operator scope by either route. Issue 340's protection
stands: a tool call without a warrant writes nothing into the operator's home.
**The tools stay the operator's way in.** While no channel answers approve for the module, such a tool
records the request as **pending** in the module's state and answers with the exact line to type,
`nox settings set claude-code '<json>'`, the request merged into the current layer so that nothing else
changes. The status tool lists pending requests; one is dropped once the layer holds it, or after 7 days.
**A hand edit is a condition.** On every look, and within a minute of a change on disk, the module compares
the rendered paths with the disk. Any difference is said in its health as a finding on that node, and the
controller raises **`agent-home-drift`**, a warning that needs the operator, naming each path:
Nothing that runs — a hook, a permission rule (`autoMode` included), a tool server, an environment or
key-helper setting, a skill or command holding a shell line or the front matter issue 340 refuses — is taken
at the operator scope by either route. Issue 340's protection stands: a tool call without a warrant writes
nothing into the operator's home. A home-scope name never shadows an operator-scope item of the same kind.
- a file nobody rendered is left and reported; the operator imports it (the import tool, at a scope) or
removes it (ADR 0243's tool, reason given, copy kept);
- a rendered file changed by hand is written back on the next render, and the hand version is kept as a
copy in the module's state, shown by the status tool;
- a rendered file missing is rendered again.
**A hand edit is a condition, undone on the look that finds it.** At least once a minute, and at once on a
change on disk, the module compares the rendered paths with the disk and reads the two watched entries. Any
difference is said in its health as a finding on that node, and the controller raises **`agent-home-drift`**,
a warning that needs the operator, naming each path. That look acts:
- a rendered file changed by hand (`settings.json` above all) is written back, the hand version kept in
`home-copies` and shown by the status tool; only a preference key the program changed is taken over;
- a rendered file missing is rendered again;
- a file nobody rendered under `rules/`, `skills/`, `commands/`, `agents/`, `hooks/` or `output-styles/` is,
**before the node's first adoption** (the imports below, recorded once per node), left and reported for the
operator to import or remove (ADR 0243); **after it**, moved into `home-copies`, restorable, and raised;
- an entry under neither list, a key other than `permissions` in `settings.local.json`, and a marketplace or
plugin in `plugins/` other than the vendor's and `nox-mesh` are raised, naming each, and left.
The condition clears on the first clean look; the first-node gate never reads it as a fault of a build
([ADR 0254](../../02-DECISIONS/0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md)'s
exemption). **The guard** (§3) refuses any shell command or file edit that writes, appends to, copies or
moves into, edits in place or removes a rendered path, naming the module's tool and saying a rule the agent
wants is a lesson. It does not watch the working state. The operator's override (ADR 0245 §5) applies.
exemption). **The guard** (§3) refuses, naming the module's tool and saying a rule the agent wants is a
lesson: any shell command or file edit that writes, appends to, copies or moves into, edits in place or
removes a rendered path, `settings.local.json` or a `.json` file under `plugins/`; and `claude plugin
install`, `claude plugin marketplace add` and their in-session forms. It does not watch the rest of the
working state. The operator's override (ADR 0245 §5) applies.
**The first registrations** are imports: the operator's two rule files on the laptop as the first personal
rules; the laptop's skills and the workstation's subagent imported or removed on the operator's word; the
laptop's `settings.json` as the operator's settings; the retired `cleanup` skill and the three stale
instruction files found on four nodes on 2026-10-09 removed with copies kept. Each is a condition until
imported or removed.
**The first registrations** are imports, the node's first adoption: the operator's two rule files on the
laptop as the first personal rules; the laptop's skills and the workstation's subagent imported or removed on
the operator's word; the preference keys of the laptop's `settings.json` as the operator's settings, and its
`autoMode` key moved to the managed settings at the terminal; `.hal-binding.json` and the credentials copy
removed on the operator's word; the retired `cleanup` skill and the three stale instruction files found on
four nodes on 2026-10-09 removed with copies kept. Each is a condition until imported or removed.
**Lessons.** The module keeps a `lessons` register in its state: per lesson, when and on which node it was
proposed, the session where known, the **trigger** (the operator's words), the **cause** (what the agent did),
@@ -450,7 +468,10 @@ the **rule** (a personal rule in the operator's second person), an optional **be
prefix or path pattern), and its state — `proposed`, `accepted` with who, how and when, or `declined` with the
reason. The mesh's instruction section tells every agent to record a lesson, in the same turn, when the
operator expresses frustration or corrects a way of working, after first working out why, and to say in one
line that it did.
line that it did. A correction goes to the register, not to the agent's own per-project memory, which stays its
notebook for facts about a project. **The first lessons are the agent's existing corrections**: once the
register exists on a node, the agent there proposes, unasked, every note in its own memory that records a
correction the operator gave, and says so in one line.
- **A proposed lesson is not a rule.** It is rendered nowhere. The module shows it to the operator for a yes:
an approve ask with Accept and Decline, or, while no channel answers approve, the list tool shows it and the
@@ -471,7 +492,8 @@ line that it did.
**The tools of this section**: `claude_code_personal_rule_register`, `_list`, `_unregister`;
`claude_code_personal_settings_get`, `_set`; the existing register tools with `scope: operator` for skills,
commands and subagents; `claude_code_config_import` taken again, at the operator scope, by the two routes;
`claude_code_config_status` extended with the drift findings and the unknown entries;
`claude_code_config_status` extended with the drift findings, the unknown entries, the watched entries
and the pending requests;
`claude_code_lesson_register`, `_list`, `_accept`, `_decline`. Every tool that writes into the operator's
home goes through the two routes above.
@@ -490,8 +512,12 @@ home goes through the two routes above.
| the module's render test over a seeded home: every configuration path of §10 matches the registrations byte for byte, every excluded entry is byte-identical before and after, an unknown entry is reported; a program-written key of `settings.json` is taken over into the operator's settings, a key outside the list is drift | ADR 0273 §1 |
| the module's render test for one machine of three: a personal rule lands under `rules/` and in the home instruction file in the operator's own home on two and in the agent account's home on the third; a hook, a permission rule, a tool server and an item holding a shell line are refused at the operator scope | ADR 0273 §2 |
| the module's tests through its real tool dispatch: a register, import or remove at the operator scope, or for an operator's home, publishes an ask and writes nothing; with a warrant for that ask it acts once; another ask's warrant, or a replayed one, is refused; while no channel answers approve the ask is refused naming the terminal's line | ADR 0273 §3, issue 340 |
| the module's tests: a file nobody rendered is reported and left; a rendered file changed by hand is written back and its hand version kept and shown; a missing one is rendered again; the condition clears on a clean look; the controller's test that `agent-home-drift` is a warning the first-node gate never reads as a fault; the guard refuses a write, append, copy, move, in-place edit and removal under each rendered path naming the tool, and lets `projects/` alone | ADR 0273 §4 |
| the module's tests with a clock: a rendered file changed by hand (`settings.json` among them) is written back on the look that finds it, within a minute, its hand version kept and shown; a `hooks`, `permissions`, tool-server or `autoMode` key added on disk is written back and never taken over; a missing file is rendered again on that look; before a node's first adoption an unrendered file under `rules/`, `skills/`, `commands/`, `agents/`, `hooks/`, `output-styles/` is left and reported, after it the same file is moved into `home-copies`, restorable, and raised; an entry under neither list, `.hal-binding.json` and a `.credentials.json.*` copy are raised and left; a key other than `permissions` in `settings.local.json` and a marketplace or plugin other than the vendor's and `nox-mesh` are raised; the condition clears on a clean look; the controller's test that `agent-home-drift` is a warning the first-node gate never reads as a fault | ADR 0273 §1, §4 |
| the module's guard tests: a write, append, copy, move, in-place edit and removal under each rendered path, and aimed at `settings.local.json` and a `.json` under `plugins/`, refused naming the tool; `claude plugin install`, `claude plugin marketplace add` and their in-session forms refused; a write under `projects/` passes | ADR 0273 §4 |
| the module's test: a home-scope registration of a name and kind already registered at the operator scope is refused naming it, and the render holds the operator's item | ADR 0273 §3 |
| the module's test: a tool called for the operator's home writes nothing there, records one pending request and answers a `nox settings set claude-code '…'` line that, applied to the current layer, adds that item and changes no other key; the request is dropped once the layer holds it, and after 7 days | ADR 0273 §3 |
| the module's tests over the lessons register: register, list, accept, decline, expiry; only accepted lessons reach `rules/`; a proposal matching a declined rule is refused with the reason; the session-start hook prints the recent and matching rules; the guard prints a matching rule once per session and refuses nothing for it | ADR 0273 §5 |
| the module's render test: the rendered managed instruction file's `working-on-the-mesh` section holds the paragraph on when a lesson is recorded, that a correction goes to the register and not to the agent's memory, and that existing corrections are proposed first; the test fails when the paragraph is taken out | ADR 0273 §5 |
| live: on the laptop, `~/.claude` holds the rendered configuration and the working state and nothing else; a rule written by hand into `rules/` raises `agent-home-drift` within a minute; a rule set at the terminal is in a new session on every node; a correction given in a session is in the lessons list in that turn, accepted at the terminal, and named at the start of the next session on another node | ADR 0273, the exit of WP7–WP9 |
| live: a skill registered at the mesh scope is offered as `nox-mesh:<name>` in a new session on each machine | ADR 0216 |
| the module's test: the "instead of" table is generated from the controller's answers, ordered and cut; the guard refuses ssh to a mesh machine by name, domain, internal name, address, alias and jump, and the local work-arounds for a mesh name, naming the tool — and lets git to the forge, ssh beyond the mesh and a command merely mentioning ssh through; the override is honoured only from the session's start and only when recorded | ADR 0245 |
@@ -216,34 +216,47 @@ WP10 the rest of the nodes, and the as-is (the live mesh)
1. **The manifest.** The module `uses` the `operator-channel` seat, as an asker. New settings, each a list
of items whose text is a list of lines (a setting holds no line break, issue 339): `personal_rules`,
`personal_skills`, `personal_commands`, `personal_agents`, `personal_settings`, `lessons_accepted`,
`lessons_declined`. New state buckets: `personal` (the operator-scope registrations and the lessons
accepted into them), `lessons` (WP9), `home-copies` (the hand versions and the removal copies ADR 0243
already keeps). The documentation gains the two lists of design 36 §10: the rendered paths and the
excluded working state.
`personal_skills`, `personal_commands`, `personal_agents`, `personal_output_styles`,
`personal_keybindings`, `personal_settings` (preference keys only; `autoMode`, `hooks`, `permissions`,
tool-server, `env`, `apiKeyHelper` and `statusLine` refused), `lessons_accepted`, `lessons_declined`. New
state buckets: `personal` (the operator-scope registrations and the lessons accepted into them), `lessons`
(WP9), `home-copies` (the hand versions, the files moved out after adoption, and the removal copies ADR 0243
already keeps; never a credentials copy), `pending` (requests waiting for the terminal), and a per-node
record of the first adoption. The documentation gains the lists of design 36 §10: the rendered paths, the
excluded working state, the two watched entries, and the entries named as neither.
2. **The renderer.** From the operator-scope registrations and the terminal's settings, the home instruction
file, `rules/`, `skills/`, `commands/`, `agents/`, an empty `hooks/` and `settings.json`, written as the
file, `rules/`, `skills/`, `commands/`, `agents/`, `output-styles/`, an empty `hooks/`, `keybindings.json`
and `settings.json`, written as the
agent's account (through §9's helper where that is an agent account), only when content changed, and a
record of what was rendered. The program-written keys of `settings.json` taken over into `personal_settings`
when the program changes them.
3. **Drift.** A comparison on every look and on a file watch, bounded to the rendered paths; findings in the
module's health statement (ADR 0240) naming each path and what differs; the hand version of a changed file
kept in `home-copies` before it is written back; a file nobody rendered left and reported. The controller:
record of what was rendered. The program-written preference keys of `settings.json` taken over into
`personal_settings` when the program changes them; every other key written back.
3. **Drift.** A comparison at least once a minute and at once on a file watch, over the rendered paths, the
two watched entries and the directory's top level; findings in the module's health statement (ADR 0240)
naming each path and what differs. **The look that finds a difference acts**: a changed rendered file is
written back on that look, its hand version kept in `home-copies` first (a key that decides what runs is
never taken over); a missing one rendered again; a file nobody rendered under `rules/`, `skills/`,
`commands/`, `agents/`, `hooks/` or `output-styles/` left and reported before the node's first adoption,
and after it moved into `home-copies`, restorable, and raised; an entry under neither list, a
`settings.local.json` key other than `permissions`, and a marketplace or plugin other than the vendor's and
`nox-mesh` raised and left. The controller:
the condition kind `agent-home-drift`, a warning that needs the operator, exempt at the first-node gate as
the relogin and directory-used-as-found kinds are (one small change in `mesh-controller`).
4. **The two routes.** Every register, import and remove tool at the operator scope or for an operator's
home publishes an approve ask through the asker's client, with the item in full and a digest over text,
name, kind and scope, and acts once on the warrant; while the router carries no channel that answers
approve for this module, it refuses at once naming the terminal's line. The home scope into an agent
approve for this module, it records the request in `pending` and answers the exact
`nox settings set claude-code '<json>'` line, merged into the current layer so nothing else changes. A
home-scope name an operator-scope item of the same kind uses is refused. The home scope into an agent
account's own home stays as issue 340 built it.
5. **The guard.** A refusal of any shell command or file edit whose target is under a rendered path (a
redirect, append, copy, move, in-place edit, removal), naming the tool; nothing for the working state;
the override of ADR 0245 §5.
5. **The guard.** A refusal of any shell command or file edit whose target is under a rendered path, is
`settings.local.json`, or is a `.json` file under `plugins/` (a redirect, append, copy, move, in-place
edit, removal), and of `claude plugin install`, `claude plugin marketplace add` and their in-session forms,
naming the tool; nothing for the rest of the working state; the override of ADR 0245 §5.
6. **The tools.** `claude_code_personal_rule_register`, `_list`, `_unregister`;
`claude_code_personal_settings_get`, `_set`; `scope: operator` on the skill, command and agent register
tools; `claude_code_config_import` taken again at the operator scope by the two routes;
`claude_code_config_status` extended with the drift findings and the entries the module knows under
neither list.
`claude_code_config_status` extended with the drift findings, the entries the module knows under
neither list, the watched entries and the pending requests.
**Proof, before anything runs live.** The module's tests of design 36 "How it is checked" for ADR 0273 §1,
§2, §3 and §4, each failing on the commit before. The catalogue's checks pass.
@@ -252,18 +265,22 @@ WP10 the rest of the nodes, and the as-is (the live mesh)
*The live mesh. Half a day, and it is the operator's.* One workstation first, as every package here.
1. The module with WP7 reaches the laptop. `agent-home-drift` is raised there, naming the two rules, the
five skills, `settings.json` and nothing else.
2. At the terminal the operator sets `personal_rules` with the two rules, `personal_settings` with the six
keys, and `personal_skills` with the skills they keep; removes the rest with the removal tool, copy kept.
The condition clears.
1. The module with WP7 reaches the laptop, where no adoption is recorded yet. `agent-home-drift` is raised
there, naming the two rules, the five skills, `settings.json` (its keys differ from an empty registration),
`.hal-binding.json` and `.credentials.json.testbak` as entries under neither list, and nothing else.
2. At the terminal the operator sets `personal_rules` with the two rules, `personal_settings` with the five
preference keys, `managed_settings` (for the laptop or the mesh) with the `autoMode` key, which is a
permission rule and cannot be imported at the operator scope, and `personal_skills` with the skills they
keep; removes the rest, `.hal-binding.json` and the credentials copy with the removal tool (no copy kept of
the credentials copy). The operator then records the laptop's first adoption. The condition clears.
3. The module reaches the other three nodes. On each, the condition names the retired `cleanup` skill and
the instruction file (and the workstation's subagent); the operator removes or imports each, once.
the instruction file (and the workstation's subagent); the operator removes or imports each, once, and
records that node's first adoption.
**Proof.** On every node `~/.claude` holds the rendered configuration and the program's working state and
nothing else; a new session on the workstation has the laptop's two rules in its context; a rule written by
hand into `rules/` on the laptop raises the condition within a minute and is written back on the next render
with the hand version shown by the status tool.
hand into `rules/` on the laptop is moved into the copies and raised within a minute; a hand edit of
`settings.json` is written back within a minute with the hand version shown by the status tool.
### WP9 — Lessons
@@ -281,12 +298,17 @@ with the hand version shown by the status tool.
the guard printing the rule of a matching accepted lesson once per session before the tool runs,
refusing nothing for it.
4. **The instruction section**: the mesh's `working-on-the-mesh` section gains the paragraph of design 36
§10 on when an agent records a lesson, and the `talking-to-the-operator` section one line saying the agent
§10 on when an agent records a lesson, that a correction goes to the register and not to the agent's own
memory, and that its existing corrections are proposed first; a render test holds the section to it; and the `talking-to-the-operator` section one line saying the agent
says in one line that it did. Both are set at the terminal, as every mesh-wide section is since issue 340.
**Proof, before anything runs live.** The module's tests for ADR 0273 §5 in design 36 "How it is checked".
**Proof, live, on one workstation.** In a session, the operator corrects a way of working; the lesson is in
5. **The first lessons**: on the laptop, once the register exists, the agent proposes, unasked, every note in
its memory that records a correction the operator gave (at least 9 on 2026-10-09).
**Proof, live, on one workstation.** The laptop's memory corrections are in `claude_code_lesson_list` as
proposals. In a session, the operator corrects a way of working; the lesson is in
`claude_code_lesson_list` in that turn; the operator accepts it at the terminal; the next session on another
node names it at start; a tool whose name matches its `before` is preceded by the rule once.