Instance clone
Cloning has two halves. A script owns the deterministic half; the credential half is deliberately not
automated. Almost every "the clone does not work" report is the second half never having happened —
which is why the steps that were not performed are printed rather than silently skipped.
What the script does, and what it refuses to do
${CLAUDE_PLUGIN_ROOT}/scripts/clone.py <new-name> [--from <reference>] [--port auto|<n>]
[--plan | --apply --yes] [--provisioner CMD]
| Does |
Does not |
| validate the name against the fleet and the prefix |
mint a machine identity or grant it project access |
| choose a genuinely free host port |
create or copy any secret value |
| run the isolation preflight and print it, passes and failures alike |
perform a provider login |
| materialise the compose directory, the state directories and a patched config |
apply the model chain |
| create credential directories empty, mode 0700 |
populate or register skills and plugins |
| print every remaining manual step |
start anything, or give the instance work |
Exit codes: 0 planned or applied · 1 runtime · 2 fleet config missing or invalid · 3 refused · 4
reference not found · 5 isolation preflight failed. A refused preflight is the script working.
Isolation preflight
Four properties must be unique per gateway, and one more depends on the network mode:
| Requirement |
Why a clash is not loud |
| config path |
both instances read one file; an edit meant for one lands on both |
| state directory |
startup enforces unique state-directory ownership, so the second instance to start loses. The clone looks fine today and something else breaks tomorrow |
| agents workspace |
two instances writing one workspace corrupt each other's working state |
| published host port |
the second publication fails at up time, or worse, silently binds elsewhere |
| network namespace |
if the source runs with host networking, the in-container port must differ too — the published-port check does not cover that case |
An unknown value is a failed requirement. Uniqueness that could not be asserted has not been
established, and the preflight refuses rather than assuming.
A free port means free by four measures
Not published by any container on this host · not written into any compose file of the fleet — a
stopped instance still owns its port · not bindable-in-use at this moment · not already claimed by
another entry in the fleet config. Any single measure passes on a port that another instance owns.
Publish on loopback: images publish outward by default, and a clone is the easiest place to lose that.
Credentials are never copied
- OAuth material is not portable. An API key or a static token would survive a copy; an OAuth
profile does not, and copying one gives two instances a single refresh chain — the exact
configuration that logs one of them out at the next rotation (
fleet.auth.token-sink).
- Credential directories for the clone are created empty. If the fleet deliberately shares one
owner credential directory by mount, the clone mounts the same directory — sharing by mount is a
decision with one owner; sharing by copy is two owners and a race.
- The embedding key is per instance. Sharing one hides which instance burns the quota, and changing
a key later changes the index identity and forces a full reindex everywhere it was used.
- Secret references travel; secret values do not. The patched config carries the same references and
resolves none of them.
After the script: the half nobody automated
- Machine identity for the new instance, with read access to the same secret project.
- The per-instance secrets the source declares — compared by name and count, never by value.
- Provider logins registered on the new instance, from the credential owner, never copied.
- The model chain applied at all three levels: default, per agent, per schedule.
- Skills and plugins: shared paths registered; plugin load paths need a restart, not a reload.
- The fleet config entry — role, criticality, aliases — then
fleet.py config --diff clean.
- Bring it up, run the health battery, and only then give it work.
Each step with its verification and its failure mode: references/post-clone-checklist.md.
Prove the isolation, do not assume it
- Mount tables side by side. No host path of the source appears under the clone except the ones the
fleet deliberately shares.
- The rendered compose file must not name the source anywhere. A surviving occurrence means the
clone carries the source's identity into some path, label or variable.
- If the source compose file never names its own instance, it is not templated: the clone would
share every path with it. Template the deployment first; that is a prerequisite, not a nicety.
- Config points at the clone's own tree: workspace, state directory, config path, port.
- Secret delivery matches the source by key name and count. A short count is the wrong project id
or a broken identity, not a missing feature.
- The source is unchanged: same config mtime, same restart count, same port, before and after.
Common mistakes
- Cloning to get the source's state. The copy carries layout and config; memory, sessions and
schedules are not transplantable, and a config that is copied wholesale carries the source's
identity with it.
- Copying the credential directory "just to make it work". It works until the next token rotation, and
then two instances are logged out instead of one.
- Choosing the port with a single check, usually "nothing is listening". Stopped instances own ports.
- Cloning whatever instance was nearest. Clone the reference; a clone inherits every unfixed
finding, and a clone of a zombie is a second zombie. If the reference is not healthy, fix it first.
- Cloning the legacy instance. Its layout is a different deployment shape, every mutation on it is
refused, and re-creating it multiplies a migration project.
- Passing
--yes in the same turn the plan was first shown, or --apply without reading the printed
preflight rows — which are printed on success too, precisely so that they are read.
- Adding the clone to the fleet config after handing it work. Until it is in the config it is
unmanaged, and every mutation on it is refused — discovered at the least convenient moment.
1---2name: instance-clone3description: Use when a new OpenClaw instance is to be created from an existing one — cloning the reference, standing up a canary or a throwaway test instance, adding an instance for a new workload or tenant, picking a free gateway port for one, deciding what a new instance may share with its source and what it must not — and also when an instance created earlier behaves like its source, answers on the wrong port, has no credentials, or is suspected of not being isolated from the instance it was copied from.4---56# Instance clone78Cloning has two halves. A script owns the deterministic half; the credential half is deliberately not9automated. Almost every "the clone does not work" report is the second half never having happened —10which is why the steps that were not performed are **printed** rather than silently skipped.1112## What the script does, and what it refuses to do1314```15${CLAUDE_PLUGIN_ROOT}/scripts/clone.py <new-name> [--from <reference>] [--port auto|<n>]16 [--plan | --apply --yes] [--provisioner CMD]17```1819| Does | Does not |20|---|---|21| validate the name against the fleet and the prefix | mint a machine identity or grant it project access |22| choose a genuinely free host port | create or copy any secret value |23| run the isolation preflight and print it, passes and failures alike | perform a provider login |24| materialise the compose directory, the state directories and a patched config | apply the model chain |25| create credential directories **empty**, mode 0700 | populate or register skills and plugins |26| print every remaining manual step | start anything, or give the instance work |2728Exit codes: 0 planned or applied · 1 runtime · 2 fleet config missing or invalid · 3 refused · 429reference not found · 5 isolation preflight failed. A refused preflight is the script working.3031## Isolation preflight3233Four properties must be unique per gateway, and one more depends on the network mode:3435| Requirement | Why a clash is not loud |36|---|---|37| config path | both instances read one file; an edit meant for one lands on both |38| state directory | startup **enforces unique state-directory ownership**, so the second instance to start loses. The clone looks fine today and something else breaks tomorrow |39| agents workspace | two instances writing one workspace corrupt each other's working state |40| published host port | the second publication fails at `up` time, or worse, silently binds elsewhere |41| network namespace | if the source runs with host networking, the **in-container** port must differ too — the published-port check does not cover that case |4243**An unknown value is a failed requirement.** Uniqueness that could not be asserted has not been44established, and the preflight refuses rather than assuming.4546## A free port means free by four measures4748Not published by any container on this host · not written into any compose file of the fleet — a49**stopped** instance still owns its port · not bindable-in-use at this moment · not already claimed by50another entry in the fleet config. Any single measure passes on a port that another instance owns.51Publish on loopback: images publish outward by default, and a clone is the easiest place to lose that.5253## Credentials are never copied5455- **OAuth material is not portable.** An API key or a static token would survive a copy; an OAuth56 profile does not, and copying one gives two instances a single refresh chain — the exact57 configuration that logs one of them out at the next rotation (`fleet.auth.token-sink`).58- Credential directories for the clone are created **empty**. If the fleet deliberately shares one59 owner credential directory by mount, the clone mounts the same directory — sharing by mount is a60 decision with one owner; sharing by copy is two owners and a race.61- The embedding key is **per instance**. Sharing one hides which instance burns the quota, and changing62 a key later changes the index identity and forces a full reindex everywhere it was used.63- Secret references travel; secret values do not. The patched config carries the same references and64 resolves none of them.6566## After the script: the half nobody automated67681. Machine identity for the new instance, with read access to the same secret project.692. The per-instance secrets the source declares — compared **by name and count**, never by value.703. Provider logins registered on the new instance, from the credential owner, never copied.714. The model chain applied at all three levels: default, per agent, per schedule.725. Skills and plugins: shared paths registered; plugin load paths need a **restart**, not a reload.736. The fleet config entry — role, criticality, aliases — then `fleet.py config --diff` clean.747. Bring it up, run the health battery, and only then give it work.7576Each step with its verification and its failure mode: `references/post-clone-checklist.md`.7778## Prove the isolation, do not assume it7980- **Mount tables side by side.** No host path of the source appears under the clone except the ones the81 fleet deliberately shares.82- **The rendered compose file must not name the source anywhere.** A surviving occurrence means the83 clone carries the source's identity into some path, label or variable.84- **If the source compose file never names its own instance**, it is not templated: the clone would85 share every path with it. Template the deployment first; that is a prerequisite, not a nicety.86- **Config points at the clone's own tree**: workspace, state directory, config path, port.87- **Secret delivery matches** the source by key name and count. A short count is the wrong project id88 or a broken identity, not a missing feature.89- **The source is unchanged**: same config mtime, same restart count, same port, before and after.9091## Common mistakes9293- Cloning to get the source's *state*. The copy carries layout and config; memory, sessions and94 schedules are not transplantable, and a config that is copied wholesale carries the source's95 identity with it.96- Copying the credential directory "just to make it work". It works until the next token rotation, and97 then two instances are logged out instead of one.98- Choosing the port with a single check, usually "nothing is listening". Stopped instances own ports.99- Cloning whatever instance was nearest. Clone the **reference**; a clone inherits every unfixed100 finding, and a clone of a zombie is a second zombie. If the reference is not healthy, fix it first.101- Cloning the legacy instance. Its layout is a different deployment shape, every mutation on it is102 refused, and re-creating it multiplies a migration project.103- Passing `--yes` in the same turn the plan was first shown, or `--apply` without reading the printed104 preflight rows — which are printed on success too, precisely so that they are read.105- Adding the clone to the fleet config after handing it work. Until it is in the config it is106 unmanaged, and every mutation on it is refused — discovered at the least convenient moment.