Sync Claude Code configuration between a local Mac and Canva devboxes. Push settings changes once, and every devbox picks them up on the next restart.
┌─────────────────────┐ sync-claude.sh ┌──────────────────────┐
│ Local Mac │ ──────────────────────► │ Dotfiles repo │
│ ~/.claude/ │ strips Mac values, │ claude/.claude/ │
│ settings.json │ adds devbox values │ settings.json │
└─────────────────────┘ └──────────┬───────────┘
│
git push│
▼
┌──────────────────────┐
│ GitHub │
│ canvanauts/ │
│ your-dotfiles │
└──────────┬───────────┘
│
coder dotfiles (every restart) │
clones/pulls repo → runs │
install.sh → playbook │
│
▼
┌─────────────────────────────────────────────┐
│ Devbox (~/.dotfiles/your-dotfiles) │
│ │
│ install.sh │
│ ├─ nix packages (stow, gh, etc.) │
│ ├─ Claude CLI + MCP │
│ ├─ ansible-playbook startup-playbook.yml │
│ │ ├─ stow configs │
│ │ ├─ copy Claude settings + skills │
│ │ ├─ direnv allow │
│ │ ├─ VSCode machine settings │
│ │ └─ clone Canva repo + fetch │
│ ├─ corepack + pnpm install │
│ └─ playwright-cli │
└─────────────────────────────────────────────┘
│
│ read-modify-write (preserves custom keys)
▼
┌─────────────────────┐
│ Otter │
│ Adds: apiKeyHelper, │
│ env vars, plugins │
└─────────────────────┘
| File | Description |
|---|---|
settings.json |
Example local Mac ~/.claude/settings.json — source of truth for personal settings |
settings-devbox.json |
Example generated devbox config — Mac values stripped, devbox values added |
sync-claude.sh |
Mac → devbox settings transform script (auto-commits and pushes) |
startup-playbook.yml |
Ansible playbook called by install.sh — stows configs, copies Claude settings, clones Canva repo |
install.sh |
Entry point for devbox setup — nix packages, Claude CLI, calls playbook, pnpm install |
.bash_aliases |
Shell aliases and startup reminders (stowed to ~/) |
Create a private GitHub repo (e.g., canvanauts/your-dotfiles) with this structure:
your-dotfiles/
├── bash/
│ └── .bash_aliases # stowed to ~/
├── claude/
│ └── .claude/
│ ├── settings.json # devbox Claude settings (generated)
│ └── skills/ # user-level skills (copied from Mac)
├── git/ # optional: stowed git config
├── startup-playbook.yml # Ansible playbook (called by install.sh)
├── install.sh # entry point (called by coder dotfiles)
└── sync-claude.sh # Mac → devbox settings transform
When creating a devbox, set the dotfiles URL to your canvanauts repo (e.g., https://github.com/canvanauts/your-dotfiles). The devbox will:
- Clone your repo on every restart via
coder dotfiles - Run
install.shautomatically
This is all you need — no ~/.sync/ setup required.
Your install.sh is the entry point called by coder dotfiles. It should:
- Install nix packages first (so
stowis available for the playbook) - Install Claude Code CLI
- Call
ansible-playbook startup-playbook.ymlfor declarative config - Run procedural steps (corepack, pnpm install, etc.)
#!/usr/bin/env bash
set -euox pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 1. Nix packages (installs stow, gh, etc.)
nix-env -iA nixpkgs.stow nixpkgs.gh nixpkgs.tmux
# 2. Claude Code CLI
if ! command -v claude >/dev/null 2>&1; then
curl -fsSL https://claude.ai/install.sh | bash
fi
# 3. Ansible playbook (stow, Claude configs, Canva repo clone)
ansible-playbook "$SCRIPT_DIR/startup-playbook.yml"
# 4. Procedural steps (pnpm install, etc.)
if command -v corepack >/dev/null 2>&1; then
corepack enable
fi
# ... pnpm install, playwright, etc.The playbook handles declarative setup. It uses playbook_dir (an Ansible built-in) to reference files in the repo:
- hosts: localhost
connection: local
collections:
- devbox.core
- devbox.community
vars:
dotfiles_dir: "{{ playbook_dir }}"
tasks:
- name: Stow bash aliases
ansible.builtin.shell: stow -d {{ dotfiles_dir }} bash -t ~
ignore_errors: yes
- name: Copy Claude Code settings
ansible.builtin.copy:
src: "{{ dotfiles_dir }}/claude/.claude/settings.json"
dest: "{{ ansible_env.HOME }}/.claude/settings.json"
mode: "0644"
- name: Clone Canva repo
ansible.builtin.git:
repo: "git@github.com:canva/canva.git"
dest: "{{ ansible_env.HOME }}/work/canva"
update: no
clone: yes
ignore_errors: yesStart with your local ~/.claude/settings.json and adapt sync-claude.sh to strip Mac-specific values. The key transforms are:
- Remove
apiKeyHelper(Otter manages auth on devboxes) - Remove Mac-only env vars (
CLAUDE_CODE_SSE_PORT,CLAUDE_CODE_SKIP_BEDROCK_AUTH) - Remove Mac absolute paths from permissions (
/Users/...) - Replace
afplaynotification sounds withtput bel - Remove Mac-only hooks (PostToolUse, ConfigChange for local sync)
- Add
"allowDangerouslySkipPermissions": true(devbox only)
The generated output goes to claude/.claude/settings.json in your dotfiles repo.
Add these hooks to your local Mac ~/.claude/settings.json so that when Claude Code modifies your settings or skills, the devbox config is regenerated and pushed automatically. Merge this into your existing hooks key:
What this does: Whenever you change a Claude Code setting or skill on your Mac — whether Claude edits the file directly or you change it via the settings UI — sync-claude.sh runs automatically, transforms the config for devbox use, and pushes it to your dotfiles repo. Your devboxes pick it up on next restart.
Note: Replace
~/work/your-dotfiles/sync-claude.shwith the actual path to your local clone of the dotfiles repo.
The coder dotfiles system follows the GitHub Codespaces convention — it clones the repo and runs install.sh. By having install.sh call the Ansible playbook, everything lives in one repo with one distribution mechanism (GitHub). No need for ~/.sync/ or S3 sync.
Bash handles procedural logic well (conditional installs, hash comparisons, command checks). Ansible handles declarative setup well (ensure files exist, clone repos, copy configs). Each tool plays to its strengths.
Claude Code writes runtime state to ~/.claude/. Otter (Canva's Claude Code launcher) does read-modify-write on ~/.claude/settings.json — Go's os.WriteFile() replaces symlinks with regular files, breaking stow links. Using cp avoids this entirely.
Otter uses read-modify-write on ~/.claude/settings.json every time otter claude-code starts. It adds apiKeyHelper, env vars, and enabledPlugins but preserves all other keys. As long as your settings are in the file before Otter runs, they survive.
~/.claude/settings.json— for CLI usage (otter claude-codein terminal)~/.vscode-server/data/Machine/settings.json— for the VSCode extension
The VSCode machine settings only exist on the remote side (inside ~/.vscode-server/), so they never apply locally on your Mac.
Settings auto-sync via hooks, or manually:
bash ~/work/your-dotfiles/sync-claude.shSet your dotfiles URL in workspace settings to https://github.com/canvanauts/your-dotfiles, then create or restart a devbox.
Restart the devbox service to pick up changes:
sudo systemctl restart devbox@coder.service