Sync your first two machines
Publish your Claude Code and Codex setup from one machine and keep a second machine in sync with it every day.
At the end of this tutorial, machine A publishes your Claude Code and Codex setup, and machine B applies it on its own every day. Machine A becomes the Source Machine. Machine B becomes a Follower Machine.
You sync:
| On machine A | What machine B gets |
|---|---|
~/.claude/CLAUDE.md | the same file |
~/.codex/AGENTS.md | the same file |
two keys in ~/.claude/settings.json | those keys; B's other settings stay |
one MCP server in ~/.claude.json | that server entry; B's account data stays |
two keys in ~/.codex/config.toml | those keys; B's other settings stay |
the skill ~/.claude/skills/code-review | the skill, for Claude Code and for Codex |
Before you start
- Node.js 24 or newer and npm on both machines, installed for the user account you run Claude Code and Codex under. Get Node.js from nodejs.org or your package manager.
- Linux or macOS with a POSIX shell. The commands below use one. For Windows, see Platform differences.
- SSH from B to A. The Source Machine listens only on its own loopback address, so B reaches it through an SSH tunnel that Canonfig manages. You need an SSH login on A and A's SSH host public key. If A and B are two user accounts on one host, you need no SSH.
- Claude Code and Codex installed on B. Canonfig manages their configuration. It does not install the clients.
- The same Canonfig release on both machines. A Source Machine and a
Follower Machine on different
major.minorreleases refuse to talk to each other.
The examples use a.example and b.example as host names and maria as the
SSH user on A. Replace them with your own.
1. Install Canonfig on both machines
Run on A and on B:
npm install --global @microck/canonfig@5.0.1
canonfig --version
canonfig doctor --no-inputcanonfig --version prints 5.0.1. doctor prints one line per check. Look
at the credentials line: Canonfig keeps its keys in the system credential
store (Secret Service on Linux, Keychain on macOS). If that line reports a
problem, follow the fix it prints, or see
Run Canonfig unattended, before you continue.
2. Make machine A the Source Machine
On A:
canonfig source initThe output starts with source.init completed and lists the signing key and
TLS fingerprints. Nothing is published yet. Running the command again creates
nothing; it checks that the stored keys still match.
3. Write the Machine Profile
A Machine Profile is a JSONC file that lists each Profile Resource:
what to manage (kind), where it goes on the Follower Machine (target),
where its content comes from (spec), and how to check it (verify).
Create ~/canonfig.profile.jsonc on A. Because the file sits in your home
directory, each source path points at the real file, so after you edit
~/.claude/CLAUDE.md you only need to publish again.
{
// The profile file format. Always 2.
"version": 2,
"id": "workstation",
"name": "Workstation",
"groups": [],
"resources": [
// Whole files. "source" is relative to this profile file's directory.
{
"id": "claude-instructions",
"kind": "file",
"target": "~/.claude/CLAUDE.md",
"spec": { "kind": "file", "source": ".claude/CLAUDE.md" },
"verify": { "method": "digest" }
},
{
"id": "codex-instructions",
"kind": "file",
"target": "~/.codex/AGENTS.md",
"spec": { "kind": "file", "source": ".codex/AGENTS.md" },
"verify": { "method": "digest" }
},
// Selected keys in Claude Code settings. Keys you do not list stay as they are on B.
{
"id": "claude-settings",
"kind": "config",
"target": "~/.claude/settings.json",
"spec": {
"kind": "config",
"format": "json",
"keys": [
{ "path": "permissions.allow", "value": ["Bash(npm test:*)", "Bash(git diff:*)"] },
{
"path": "hooks.PostToolUse",
"value": [
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "git diff --stat" }] }
]
}
]
},
"verify": { "method": "digest" }
},
// One MCP server. ~/.claude.json also holds Claude Code account state,
// so manage only the entry you want, never the whole file.
{
"id": "claude-mcp",
"kind": "config",
"target": "~/.claude.json",
"spec": {
"kind": "config",
"format": "json",
"keys": [
{
"path": "mcpServers.memory",
"value": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"] }
}
]
},
"verify": { "method": "digest" }
},
// Codex settings in TOML: the same MCP server under Codex's own key name.
{
"id": "codex-config",
"kind": "config",
"target": "~/.codex/config.toml",
"spec": {
"kind": "config",
"format": "toml",
"keys": [
{ "path": "model_reasoning_effort", "value": "high" },
{
"path": "mcp_servers.memory",
"value": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"] }
}
]
},
"verify": { "method": "digest" }
},
// A skill is a directory. List every file it contains.
// Claude Code reads ~/.claude/skills; Codex reads ~/.agents/skills.
{
"id": "claude-review-skill",
"kind": "skill",
"target": "~/.claude/skills/code-review",
"spec": {
"kind": "skill",
"name": "code-review",
"files": [{ "path": "SKILL.md", "source": ".claude/skills/code-review/SKILL.md" }]
},
"verify": { "method": "digest" }
},
{
"id": "codex-review-skill",
"kind": "skill",
"target": "~/.agents/skills/code-review",
"spec": {
"kind": "skill",
"name": "code-review",
"files": [{ "path": "SKILL.md", "source": ".claude/skills/code-review/SKILL.md" }]
},
"verify": { "method": "digest" }
}
]
}The example follows these rules:
"verify": { "method": "digest" }without adigestvalue tells Canonfig to compute the digest from the content it publishes. If you write a digest yourself, publishing fails when it does not match.- A
sourcepath is relative to the profile file's directory, contains no.., and must stay inside that directory after symlinks are followed. - A
configkey such asmcpServers.memoryowns the whole value at that path. Canonfig writes it on B, and removes it if you later drop it from the profile. The rest of the file belongs to B. - Secrets stay out of the profile. A token belongs in the client's own login, or in a shared secret.
The profile file reference lists every field.
4. Check the profile
On A:
canonfig source digest --profile-file ~/canonfig.profile.jsoncThe output starts with source.digest completed and lists each resource with
its content digest. Nothing is published. A missing source file, a path
outside the profile directory, or an invalid value fails here with the
resource ID and the reason. Fix the profile and run the command again.
5. Publish
On A:
canonfig source publish --profile-file ~/canonfig.profile.jsonc --reviewer "$USER"The output starts with source.publish completed, then names the profile, the
sequence number (sequence 1), and each resource. Publishing unchanged
content again returns the same Profile Revision. Changed content becomes the
next sequence number.
6. Keep the Source Machine serving
B fetches Profile Revisions from A, so A must be serving whenever B syncs. On A, install the user service:
canonfig source service install
canonfig source service statusinstall returns once A answers on https://127.0.0.1:17342. status
reports running. The service is a systemd user unit
(canonfig-source.service) on Linux and a LaunchAgent (dev.canonfig.source)
on macOS, and it runs while you are logged in. On macOS, run install from the
desktop session, not over SSH. To keep A serving after logout or reboot, see
Run Canonfig unattended.
7. Invite machine B
On A:
canonfig source invite --endpoint https://127.0.0.1:17342 --output ~/canonfig-invite --expires 1hThe command writes ~/canonfig-invite. The invitation works once and expires
after one hour (without --expires, after 15 minutes; at most 24 hours).
Treat it like a password until B has enrolled. Copy it to B, together with
A's SSH host public key read from A's own disk:
scp ~/canonfig-invite b.example:canonfig-invite
scp /etc/ssh/ssh_host_ed25519_key.pub b.example:source-host-key.pub8. Enroll machine B
On B, start the tunnel to A. Skip this command if A and B are the same host.
canonfig tunnel start --invitation ./canonfig-invite --ssh-host a.example --ssh-user maria --ssh-host-key-file ./source-host-key.pubThe tunnel forwards B's 127.0.0.1:17342 to A over SSH, and checks A's SSH
host key, TLS certificate, and signing key against the values you supplied.
Canonfig records the tunnel, so later you restart it with
canonfig tunnel start alone.
Enroll B, then delete the invitation. The invitation goes through a pipe, never as an argument:
cat ./canonfig-invite | canonfig follower enroll --stdin --name laptop --profile workstation
rm ./canonfig-inviteB trusts only the Source Machine named in the invitation, stores its own
credential in its credential store, and follows the workstation profile.
canonfig status shows the trusted signing and TLS fingerprints.
9. Preview, then apply
On B:
canonfig sync --planThe plan lists one action per resource, for example write-file for
~/.claude/CLAUDE.md and write-config with the keys it sets. Nothing on B
changes. When the plan matches what you expect, apply it:
canonfig sync --applyCanonfig applies every action, then checks each resource. The result is
Converged when every resource matches. If a resource fails, Canonfig rolls
the run back and names the resource and the reason; see
Recover from a failed or blocked sync.
Run canonfig sync --apply a second time. It reports Converged with no
changes.
10. Accept the configuration in each client
Converged means the files are in place and verified. Each client still asks
you to trust some of what it receives, and Canonfig does not copy those
decisions from A:
- Claude Code. Restart Claude Code on B. If it reports that hooks changed
outside the app, open
/hooksand review them. Run/mcpto see thememoryserver. Claude Code asks whether you trust each project folder the first time you open it. - Codex. Run
codex mcp listto see thememoryserver. Codex asks you to trust each project directory the first time. Codex stores that decision in~/.codex/config.toml. Canonfig leaves keys it does not manage alone, so the decision survives later syncs.
When an apply writes Codex hooks, the output ends with "still to do in each client" and names the review you need to do.
11. Sync B every day
On B:
canonfig schedule set daily@09:00
canonfig schedule statusschedule set prints the time zone it used and the next run time.
schedule status reports current. At 09:00 each day, a systemd user timer
(Linux) or launchd agent (macOS) runs
canonfig sync --apply --no-input --scheduled. No Canonfig process runs in
between.
The schedule runs only when the machine can run it:
- Linux: while you are logged in, unless you turn on lingering with
loginctl enable-linger "$USER". - macOS: while you are logged in to the desktop with the login Keychain unlocked.
Sync on a schedule covers time zones, daylight saving time, and missed runs.
After the first scheduled run, check B:
canonfig statusapplied and independentlyVerified read verified, and scheduled reads
verified once a scheduled run has completed. If the last scheduled run
failed, status shows the reason.
12. Change something and watch it arrive
On A, edit ~/.claude/CLAUDE.md, then publish again:
canonfig source publish --profile-file ~/canonfig.profile.jsonc --reviewer "$USER"The output shows sequence 2. B applies it at its next scheduled run, or
immediately when you run canonfig sync --apply on B.
To stop managing something, remove its resource and publish. B removes the file, keys, or skill Canonfig wrote, as long as they still match what Canonfig last applied; a copy someone changed on B stays. Canonfig never removes installed tools or local credentials.
On B, a local edit to a file resource is overwritten at the next apply. A
local edit to a skill is kept and reported as Follower Drift. To change shared
content, change it on A.
If a step fails
| Message | Cause | Fix |
|---|---|---|
"Invitation duration must not exceed 24h" (source invite, exit 2) | --expires is longer than 24 hours. | Pick a value such as 15m, 1h, or 24h. |
| "the invitation has expired; ask the Source Machine for a new one" (exit 5) | B used the invitation after its --expires time. | Run canonfig source invite on A again, copy the file, and enroll with it. |
| "the invitation was already used; ask the Source Machine for a new one" (exit 5) | Each invitation enrolls one machine once. | Issue another invitation on A. |
| "the Source Machine's certificate fingerprint does not match the invitation" | B reached a server whose TLS certificate is not the one in the invitation: another Source Machine on the same port, or an invitation from an earlier canonfig source init. | On A, run canonfig source service status to confirm which Source Machine is serving, then issue an invitation from it. Do not enroll until the fingerprints match. |
"SSH host identity for a.example did not match its pin" (tunnel start) | The host key file does not belong to the host you connected to. | Copy /etc/ssh/ssh_host_ed25519_key.pub from A again, reading it on A itself, and check that --ssh-host names A. |
| "nothing answered at the Source endpoint" (exit 6) | Nothing listens on 127.0.0.1:17342: A is not serving, or the tunnel is down. | On A, run canonfig source service status and expect running. On B, run canonfig tunnel status, then canonfig tunnel start. If A serves on another port, use that port in --endpoint and in tunnel start --local-port. |
| "source/follower version mismatch" (exit 2) | A and B run different major.minor releases. The message names both. | Install the same release on both machines, as in step 1. |
| "the follower identity is already enrolled" | A Follower Machine named laptop is already enrolled. | Pick another --name, or revoke the old one on A first; see Follower credentials. |
For other errors, run canonfig doctor --no-input on the machine that failed.
Doctor probes and exit codes lists every check.
Next steps
Canonfig
Keep Claude Code, Codex, and other AI agent configuration the same on every machine you use, published from one machine you control.
Publish a change to your Follower Machines
Check and publish a Machine Profile change on the Source Machine, apply it on Follower Machines, add tools, and roll content back.