Canonfig
Tutorials

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 AWhat machine B gets
~/.claude/CLAUDE.mdthe same file
~/.codex/AGENTS.mdthe same file
two keys in ~/.claude/settings.jsonthose keys; B's other settings stay
one MCP server in ~/.claude.jsonthat server entry; B's account data stays
two keys in ~/.codex/config.tomlthose keys; B's other settings stay
the skill ~/.claude/skills/code-reviewthe 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.minor releases 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-input

canonfig --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 init

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

~/canonfig.profile.jsonc
{
  // 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 a digest value 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 source path is relative to the profile file's directory, contains no .., and must stay inside that directory after symlinks are followed.
  • A config key such as mcpServers.memory owns 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.jsonc

The 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 status

install 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 1h

The 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.pub

8. 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.pub

The 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-invite

B 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 --plan

The 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 --apply

Canonfig 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 /hooks and review them. Run /mcp to see the memory server. Claude Code asks whether you trust each project folder the first time you open it.
  • Codex. Run codex mcp list to see the memory server. 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 status

schedule 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 status

applied 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

MessageCauseFix
"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

On this page