Skip to content

Instantly share code, notes, and snippets.

@sarahbethfederman
Last active February 23, 2026 05:40
Show Gist options
  • Select an option

  • Save sarahbethfederman/b1185ab4397523c888af967ff11dfc75 to your computer and use it in GitHub Desktop.

Select an option

Save sarahbethfederman/b1185ab4397523c888af967ff11dfc75 to your computer and use it in GitHub Desktop.
Claude Code settings and sync script for devbox
# This file (~/.bash_aliases) is sourced by Ubuntu's default ~/.bashrc
alias bat=batcat
alias fd=fdfind
# Claude Code — use otter for Bedrock auth
alias claude='otter claude-code'
# Custom start command for web editor dev server
# Startup reminders
if ! gh auth status &>/dev/null 2>&1; then
echo "⚠ GitHub CLI not authenticated. Run: gh auth login"
fi
# Default to the web workspace
cd ~/work/canva/web 2>/dev/null
start() {
if [ "$1" = "editor" ]; then
USER_LOCALE=en pnpm webpack-dev-server -e editor
else
echo "Unknown command: start $1"
return 1
fi
}

Claude Code Settings & Devbox Dotfiles Sync

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.

How it works

┌─────────────────────┐     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   │
                       └─────────────────────┘

Files

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 ~/)

Setting it up yourself

1. Create a dotfiles repo

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

2. Set your dotfiles URL in workspace settings

When creating a devbox, set the dotfiles URL to your canvanauts repo (e.g., https://github.com/canvanauts/your-dotfiles). The devbox will:

  1. Clone your repo on every restart via coder dotfiles
  2. Run install.sh automatically

This is all you need — no ~/.sync/ setup required.

3. Structure install.sh as the entry point

Your install.sh is the entry point called by coder dotfiles. It should:

  1. Install nix packages first (so stow is available for the playbook)
  2. Install Claude Code CLI
  3. Call ansible-playbook startup-playbook.yml for declarative config
  4. 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.

4. Create the Ansible playbook

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: yes

5. Create your devbox Claude settings

Start 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 afplay notification sounds with tput 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.

6. Add sync hooks to your Mac Claude config

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:

// Add to your ~/.claude/settings.json on Mac
{
  "hooks": {
    // Trigger sync when Claude edits a settings or skills file
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$TOOL_INPUT\" | jq -e '.file_path | test(\"\\\\.claude/(skills/|settings)\")' >/dev/null 2>&1; then bash ~/work/your-dotfiles/sync-claude.sh; fi",
            "timeout": 30,
            "async": true
          }
        ]
      }
    ],
    // Trigger sync when Claude Code's own config changes (settings UI, /allowed, etc.)
    "ConfigChange": [
      {
        "matcher": "user_settings|local_settings|skills",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/work/your-dotfiles/sync-claude.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

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.sh with the actual path to your local clone of the dotfiles repo.

Key design decisions

Why use install.sh as the entry point?

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.

Why split between bash and Ansible?

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.

Why copy Claude configs, not symlink?

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.

How does Otter interact with custom settings?

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.

Why set allowDangerouslySkipPermissions in two places?

  • ~/.claude/settings.json — for CLI usage (otter claude-code in 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.

Usage

Updating settings from Mac

Settings auto-sync via hooks, or manually:

bash ~/work/your-dotfiles/sync-claude.sh

First-time devbox setup

Set your dotfiles URL in workspace settings to https://github.com/canvanauts/your-dotfiles, then create or restart a devbox.

Existing devboxes

Restart the devbox service to pick up changes:

sudo systemctl restart devbox@coder.service
#!/usr/bin/env bash
# This file is executed by devboxes when they start. You can do
# nearly anything in here but the rule of thumb here is for
# any actions that are executed must be able to be executed
# multiple times (ie. idempotent) without erroring out.
set -euox pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Step 1:
### Operating system level packages can be installed by Ubuntu or via Nix (or both)
###
### a. Installation of packages via Ubuntu
###
### sudo apt-get update && sudo apt-get -y install btop
###
### b. Installation of packages via Nix (identify the package name via https://search.nixos.org/packages)
###
### nix-env -iA nixpkgs.btop
# Install common tools via nix
nix-env -iA nixpkgs.diff-so-fancy \
nixpkgs.btop \
nixpkgs.gh \
nixpkgs.tmux \
nixpkgs.stow
# Step 2:
## Install Claude Code and configure for devbox Bedrock auth.
if ! command -v claude >/dev/null 2>&1; then
curl -fsSL https://claude.ai/install.sh | bash
fi
# Set up MCP servers for Claude Code
if command -v otter >/dev/null 2>&1; then
if ! claude mcp list 2>/dev/null | grep -q "otter"; then
claude mcp add otter otter mcp serve 2>/dev/null || true
fi
fi
# Step 3:
## Run Ansible playbook for declarative config (stow, Claude settings, Canva repo clone).
## Nix must run first so stow is available for the playbook.
ansible-playbook "$SCRIPT_DIR/startup-playbook.yml"
# Step 4:
## Install dependencies for the Canva repo (cloned by the playbook above).
CANVA_REPO_DIR="${HOME}/work/canva"
if command -v corepack >/dev/null 2>&1; then
corepack enable
fi
if ! command -v pnpm >/dev/null 2>&1; then
echo "pnpm is required but was not found on PATH" >&2
exit 1
fi
LOCKFILE_PATH="${CANVA_REPO_DIR}/pnpm-lock.yaml"
LOCKFILE_STAMP="${CANVA_REPO_DIR}/.devbox-pnpm-lock.sha256"
RUN_INSTALL=false
if [ ! -d "${CANVA_REPO_DIR}/node_modules" ]; then
RUN_INSTALL=true
fi
if [ -f "${LOCKFILE_PATH}" ]; then
CURRENT_LOCK_HASH="$(sha256sum "${LOCKFILE_PATH}" | awk '{print $1}')"
PREVIOUS_LOCK_HASH=""
if [ -f "${LOCKFILE_STAMP}" ]; then
PREVIOUS_LOCK_HASH="$(cat "${LOCKFILE_STAMP}")"
fi
if [ "${CURRENT_LOCK_HASH}" != "${PREVIOUS_LOCK_HASH}" ]; then
RUN_INSTALL=true
fi
fi
if [ "${RUN_INSTALL}" = true ]; then
(
cd "${CANVA_REPO_DIR}"
pnpm install
)
if [ -f "${LOCKFILE_PATH}" ]; then
sha256sum "${LOCKFILE_PATH}" | awk '{print $1}' >"${LOCKFILE_STAMP}"
fi
fi
# Step 5:
## Install global tools that require pnpm/node (must run after corepack enable).
# Playwright CLI for browser automation skill
if ! command -v playwright-cli >/dev/null 2>&1; then
pnpm install -g @playwright/cli@latest
fi
{
"env": {
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "global.anthropic.claude-opus-4-6-v1",
"ANTHROPIC_MODEL": "global.anthropic.claude-sonnet-4-6",
"AWS_REGION": "ap-southeast-2",
"CLAUDE_AGENT_SDK_VERSION": "0.2.49",
"CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "60000",
"CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING": "true",
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"CLAUDE_CODE_ENTRYPOINT": "cli",
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "32768",
"CLAUDE_CODE_TEAMMATE_MODE": "tmux",
"CLAUDE_CODE_USE_BEDROCK": "1",
"DISABLE_AUTOUPDATER": "1",
"ENABLE_TOOL_SEARCH": "true",
"FORCE_COLOR": "1",
"MAX_THINKING_TOKENS": "1024",
"MCP_CONNECTION_NONBLOCKING": "true",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp"
},
"permissions": {
"allow": [
"Bash(/usr/local/bin/coder:*)",
"Bash(/usr/local/bin/taz:*)",
"Bash(GH_TOKEN=:*)",
"Bash(GITHUB_TOKEN=:*)",
"Bash(bazel:*)",
"Bash(brew list:*)",
"Bash(cat:*)",
"Bash(chmod:*)",
"Bash(claude plugin list:*)",
"Bash(code:*)",
"Bash(coder:*)",
"Bash(curl:*)",
"Bash(do echo \"=== PR #$pr ===\")",
"Bash(done)",
"Bash(echo:*)",
"Bash(find:*)",
"Bash(for:*)",
"Bash(gh:*)",
"Bash(git log:*)",
"Bash(git:*)",
"Bash(grep:*)",
"Bash(jq:*)",
"Bash(ls:*)",
"Bash(npx taz:*)",
"Bash(npx tsc:*)",
"Bash(otter mcp exec:*)",
"Bash(playwright-cli:*)",
"Bash(pnpm fin:*)",
"Bash(pnpm test:*)",
"Bash(pnpm visreg:*)",
"Bash(python3:*)",
"Bash(sed:*)",
"Bash(taz check:*)",
"Bash(taz generate:*)",
"Bash(xargs:*)",
"Read(//usr/local/bin/**)",
"Skill(otter-tools:bk-get-logs-by-url:*)",
"Skill(otter-tools:bk-read-log:*)",
"Skill(otter-tools:get-file-contents)",
"Skill(otter-tools:get-file-contents:*)",
"Skill(otter-tools:get-pull-request-reviews:*)",
"Skill(otter-tools:get-pull-request:*)",
"Skill(otter-tools:jira-create)",
"Skill(otter-tools:jira-create:*)",
"Skill(otter-tools:kb-fetch:*)",
"Skill(otter-tools:kb-search:*)",
"Skill(otter-tools:search-code)",
"Skill(otter-tools:search-code:*)",
"Skill(playwright-cli:*)"
]
},
"model": "opus",
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "tput bel"
}
]
}
]
},
"enabledPlugins": {
"otter-tools@otter-marketplace": true
},
"outputStyle": "Explanatory",
"allowDangerouslySkipPermissions": true
}
{
"apiKeyHelper": "/usr/local/bin/otter bedrock-bearer-token",
"env": {
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "global.anthropic.claude-opus-4-6-v1",
"ANTHROPIC_MODEL": "global.anthropic.claude-sonnet-4-6",
"AWS_REGION": "ap-southeast-2",
"CLAUDE_AGENT_SDK_VERSION": "0.2.49",
"CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "60000",
"CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING": "true",
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"CLAUDE_CODE_ENTRYPOINT": "cli",
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "32768",
"CLAUDE_CODE_SKIP_BEDROCK_AUTH": "1",
"CLAUDE_CODE_SSE_PORT": "44920",
"CLAUDE_CODE_TEAMMATE_MODE": "tmux",
"CLAUDE_CODE_USE_BEDROCK": "1",
"DISABLE_AUTOUPDATER": "1",
"ENABLE_TOOL_SEARCH": "true",
"FORCE_COLOR": "1",
"MAX_THINKING_TOKENS": "1024",
"MCP_CONNECTION_NONBLOCKING": "true",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp"
},
"permissions": {
"allow": [
"Read(//Users/sarahfederman/**)",
"Read(//usr/local/bin/**)",
"Bash(bash /Users/sarahfederman/work/sarah-dotfiles/sync-claude.sh)",
"Skill(otter-tools:kb-search:*)",
"Skill(otter-tools:kb-fetch:*)",
"Skill(otter-tools:bk-get-logs-by-url:*)",
"Skill(otter-tools:bk-read-log:*)",
"Skill(otter-tools:get-pull-request:*)",
"Skill(otter-tools:get-pull-request-reviews:*)",
"Skill(playwright-cli:*)",
"Bash(git:*)",
"Bash(git log:*)",
"Bash(gh:*)",
"Bash(GH_TOKEN=:*)",
"Bash(GITHUB_TOKEN=:*)",
"Bash(bazel:*)",
"Bash(pnpm fin:*)",
"Bash(pnpm test:*)",
"Bash(python3:*)",
"Bash(otter mcp exec:*)",
"Bash(claude plugin list:*)",
"Bash(code:*)",
"Bash(coder:*)",
"Bash(/usr/local/bin/coder:*)",
"Bash(/usr/local/bin/taz:*)",
"Bash(find:*)",
"Bash(jq:*)",
"Bash(chmod:*)",
"Bash(brew list:*)",
"Bash(echo:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(sed:*)",
"Bash(for:*)",
"Bash(grep:*)",
"Bash(xargs:*)",
"Bash(curl:*)",
"Bash(taz check:*)",
"Bash(taz generate:*)",
"Bash(npx taz:*)",
"Bash(npx tsc:*)",
"Bash(playwright-cli:*)",
"Bash(pnpm visreg:*)",
"Bash(do echo \"=== PR #$pr ===\")",
"Bash(done)",
"Skill(otter-tools:jira-create)",
"Skill(otter-tools:jira-create:*)",
"Skill(otter-tools:search-code)",
"Skill(otter-tools:search-code:*)",
"Skill(otter-tools:get-file-contents)",
"Skill(otter-tools:get-file-contents:*)"
],
"additionalDirectories": [
"/Users/sarahfederman",
"/Users/sarahfederman/work/canva/.agents/skills",
"/tmp",
"/Users/sarahfederman/work/sarah-dotfiles/claude/.claude",
"/Users/sarahfederman/.claude/skills",
"/Users/sarahfederman/work/sarah-dotfiles/git/.config/git",
"/Users/sarahfederman/.claude"
]
},
"model": "opus",
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "if echo \"$TOOL_INPUT\" | jq -e '.file_path | test(\"\\\\.claude/(skills/|settings)\")' >/dev/null 2>&1; then bash /Users/sarahfederman/work/sarah-dotfiles/sync-claude.sh; fi",
"timeout": 30,
"async": true
}
]
}
],
"ConfigChange": [
{
"matcher": "user_settings|local_settings|skills",
"hooks": [
{
"type": "command",
"command": "bash /Users/sarahfederman/work/sarah-dotfiles/sync-claude.sh",
"timeout": 30
}
]
}
]
},
"enabledPlugins": {
"otter-tools@otter-marketplace": true
},
"outputStyle": "Explanatory"
}
# Devbox startup playbook — called by install.sh on every devbox startup.
# The repo is cloned automatically by `coder dotfiles` when the dotfiles URL
# is set in workspace settings (canvanauts/sarah-dotfiles).
- hosts: localhost
connection: local
collections:
- devbox.core
- devbox.community
vars:
# playbook_dir is an Ansible built-in — points to the repo root
dotfiles_dir: "{{ playbook_dir }}"
# Specify packages to install using apt
devbox_apt_packages: []
# Specify any Python packages to install with pipx
devbox_pip_packages: []
roles: []
# - role: ohmyzsh
# vars:
# ohmyzsh_theme: simple
# ohmyzsh_plugins:
# - bazel
# - git
# - sudo
tasks:
# --- Stow configs ---
- name: Stow bash aliases
ansible.builtin.shell: stow -d {{ dotfiles_dir }} bash -t ~
ignore_errors: yes
- name: Stow editorconfig
ansible.builtin.shell: stow -d {{ dotfiles_dir }} editorconfig -t ~
ignore_errors: yes
- name: Stow gh config
ansible.builtin.shell: stow -d {{ dotfiles_dir }} gh -t ~
ignore_errors: yes
- name: Stow git config
ansible.builtin.shell: stow -d {{ dotfiles_dir }} git -t ~
ignore_errors: yes
# --- Claude Code configs ---
- name: Create Claude Code directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ ansible_env.HOME }}/.claude"
- "{{ ansible_env.HOME }}/.claude/skills"
- 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: Copy Claude Code skills
ansible.builtin.shell: cp -r {{ dotfiles_dir }}/claude/.claude/skills/* {{ ansible_env.HOME }}/.claude/skills/
ignore_errors: yes
# --- direnv ---
- name: Allow direnv for Canva repo
ansible.builtin.shell: direnv allow {{ ansible_env.HOME }}/work/canva/.envrc
ignore_errors: yes
# --- VSCode machine settings (devbox only) ---
# Create preemptively — ~/.vscode-server may not exist yet at boot time,
# but VSCode Remote SSH will find these settings when it connects later.
- name: Set allowDangerouslySkipPermissions in VSCode machine settings
ansible.builtin.shell: |
SETTINGS="{{ ansible_env.HOME }}/.vscode-server/data/Machine/settings.json"
mkdir -p "$(dirname "$SETTINGS")"
if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
jq '."claudeCode.allowDangerouslySkipPermissions" = true' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
else
echo '{ "claudeCode.allowDangerouslySkipPermissions": true }' > "$SETTINGS"
fi
args:
executable: /bin/bash
ignore_errors: yes
# --- Canva repo ---
- name: Create Canva work directory
ansible.builtin.file:
path: "{{ ansible_env.HOME }}/work"
state: directory
mode: "0755"
- 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: yes
- name: Fetch latest master
ansible.builtin.shell: git -C {{ ansible_env.HOME }}/work/canva fetch origin master --prune --no-tags
ignore_errors: yes
# --- Default apt/pipx tasks ---
- name: Install apt packages
become: true
when: devbox_apt_packages | length
ansible.builtin.apt:
pkg: "{{ devbox_apt_packages }}"
autoclean: true
autoremove: true
install_recommends: false
update_cache: true
state: latest
- name: Install pipx packages
with_items: "{{ devbox_pip_packages }}"
community.general.pipx:
name: "{{ item }}"
#!/usr/bin/env bash
# Regenerate devbox Claude configs from your local Mac configs.
# Run this after changing your local ~/.claude/settings.json or skills.
#
# Usage:
# bash ~/work/sarah-dotfiles/sync-claude.sh
#
# What it does:
# 1. Reads your local ~/.claude/settings.json
# 2. Strips Mac-specific values (absolute paths, afplay hooks, otter auth)
# 3. Writes the devbox-ready config into claude/.claude/ (stow-compatible)
# 4. Copies user-level skills into claude/.claude/skills/
#
# Then commit and push to canvanauts/sarah-dotfiles.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_CLAUDE="$HOME/.claude"
SYNC_CLAUDE="$SCRIPT_DIR/claude/.claude"
if ! command -v jq &>/dev/null; then
echo "Error: jq is required. Install with: brew install jq"
exit 1
fi
echo "==> Syncing Claude Code configs from ~/.claude/ to $SYNC_CLAUDE/..."
# --- settings.json ---
if [ -f "$LOCAL_CLAUDE/settings.json" ]; then
jq '
# Remove Mac-specific apiKeyHelper (devbox uses CLAUDE_CODE_USE_BEDROCK=1)
del(.apiKeyHelper) |
# Remove SSE port (let devbox pick its own)
.env |= del(.CLAUDE_CODE_SSE_PORT) |
# Remove CLAUDE_CODE_SKIP_BEDROCK_AUTH (not needed on devbox)
.env |= del(.CLAUDE_CODE_SKIP_BEDROCK_AUTH) |
# Strip Mac-absolute-path permissions (keep generic ones)
.permissions.allow |= map(
select(test("/Users/") | not)
) |
.permissions.allow |= unique |
# Strip Mac-absolute-path additionalDirectories
.permissions |= del(.additionalDirectories) |
# Replace afplay with terminal bell for devbox
# Strip Mac-only hooks (ConfigChange, PostToolUse)
if .hooks then
.hooks |= (
if .Notification then
.Notification |= map(
.hooks |= map(
if .command | test("afplay") then
.command = "tput bel"
else . end
)
)
else . end |
del(.ConfigChange) |
del(.PostToolUse)
) |
if (.hooks | length) == 0 then del(.hooks) else . end
else . end |
# Enable dangerouslySkipPermissions on devboxes only
.allowDangerouslySkipPermissions = true
' "$LOCAL_CLAUDE/settings.json" > "$SYNC_CLAUDE/settings.json"
# Remove stale settings.local.json if it exists
rm -f "$SYNC_CLAUDE/settings.local.json"
echo " Updated settings.json (stripped Mac-specific values)"
else
echo " Skipped settings.json (not found locally)"
fi
# --- User-level skills ---
if [ -d "$LOCAL_CLAUDE/skills" ] && [ "$(ls -A "$LOCAL_CLAUDE/skills" 2>/dev/null)" ]; then
rm -rf "$SYNC_CLAUDE/skills"
mkdir -p "$SYNC_CLAUDE/skills"
cp -r "$LOCAL_CLAUDE/skills/"* "$SYNC_CLAUDE/skills/"
SKILL_COUNT=$(ls -d "$SYNC_CLAUDE/skills"/*/ 2>/dev/null | wc -l | tr -d ' ')
echo " Synced $SKILL_COUNT user-level skill(s): $(ls "$SYNC_CLAUDE/skills/" | tr '\n' ' ')"
else
echo " No user-level skills to sync"
fi
echo ""
echo "==> Done. Files updated in $SYNC_CLAUDE/"
# Auto-commit if there are changes
cd "$SCRIPT_DIR"
if ! git diff --quiet claude/ 2>/dev/null || [ -n "$(git ls-files --others --exclude-standard claude/)" ]; then
git add claude/
git commit -m "Sync Claude configs ($(date +%Y-%m-%d))" 2>/dev/null
git push 2>/dev/null
echo " Committed and pushed to remote."
osascript -e 'display notification "Configs synced and pushed to remote." with title "Claude Sync" sound name "Glass"' 2>/dev/null
else
echo " No changes to commit."
osascript -e 'display notification "No changes to commit." with title "Claude Sync" sound name "Glass"' 2>/dev/null
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment