# Nf Cc Sync

> Smart one-way sync of ~/.claude config from another machine's branch into the current one (branch-per-machine - main/laptop, server, desktop). Picks the OTHER branch, applies safe defaults (additive new files, prefer-newer conflicts, key-level merge for settings.json), auto-commits dirty tree first, auto-commits + pushes the result. NEVER pushes to the other branch — pull-only. Triggers on "sync…

- **Type:** Skill
- **Install:** `agentstack add skill-llawliet11-claude-skills-toolkit-nf-cc-sync`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [llawliet11](https://agentstack.voostack.com/s/llawliet11)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [llawliet11](https://github.com/llawliet11)
- **Source:** https://github.com/llawliet11/claude-skills-toolkit/tree/main/nf-cc-sync

## Install

```sh
agentstack add skill-llawliet11-claude-skills-toolkit-nf-cc-sync
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# nf-cc-sync — Claude Code Config Sync

One-way pull from another machine's branch into the current branch of `~/.claude/`. Run from `~/.claude/`.

## Convention this skill assumes

You keep `~/.claude/` as a git repo with **one branch per machine** (a "branch-per-machine" layout). Examples:

- Laptop tracks branch `main` (or `laptop`).
- Server tracks branch `server` (or `nas`).
- A third device tracks branch `desktop`.

Each machine only ever pushes its own branch. Sync pulls another machine's branch into the current one. Adjust the allowlist in Step 2 to match your branch names.

## Hard rules

1. **Pull-only.** Never push to the OTHER branch. Never check out OTHER. Only modify the working tree of CURRENT branch.
2. **Never auto-delete files** on receiving side. If OTHER deleted a file, log it and skip.
3. **Auto-commit dirty tree first** before sync (per user agreement). Auto-commit + auto-push at end (this repo has the auto-push exception).
4. **Hooks/, statusline-command.sh, channels/, plans/ are skipped** by default. Pass `--include-machine-config` arg to opt-in.
5. **settings.json is merged at key level**, not file level (mac vs ubuntu may have different keys).
6. Always run from `~/.claude/`. Refuse if run elsewhere.

## Step 1 — Pre-flight

```bash
cd ~/.claude
test -d .git || { echo "Not a git repo"; exit 1; }

OS=$(uname -s)
CURRENT=$(git branch --show-current)
```

Sanity: if your branch naming convention encodes OS (e.g. macOS branches start with `m`, Linux branches with `s`), check `OS` against `CURRENT` and warn via `AskUserQuestion` if they disagree. Skip this step if your convention does not encode OS.

If `git status --porcelain` is non-empty:
```bash
git add -A
git commit -m "chore(${CURRENT}): wip auto-commit before nf-cc-sync"
git push
```
Use the existing branch name as commit prefix per your global git-commit safety rule.

```bash
git fetch origin --prune
```

### Memory submodule sync (independent of branch-per-machine)

`memory/` is a submodule pointing to `llawliet11/claude-memory` (single `main` branch, shared across all machines). Synced separately from parent repo branches.

```bash
# Init if missing (first run on a freshly-cloned machine)
if [ ! -e memory/.git ]; then
  git submodule update --init --recursive memory
fi

# Reconcile local memory repo with origin/main
(
  cd memory
  git fetch origin --quiet
  LOCAL=$(git rev-parse HEAD)
  REMOTE=$(git rev-parse origin/main)
  BASE=$(git merge-base HEAD origin/main 2>/dev/null || echo "")
  if [ "$LOCAL" = "$REMOTE" ]; then
    echo "memory: up to date ($LOCAL)"
  elif [ "$LOCAL" = "$BASE" ]; then
    git pull --ff-only --quiet && echo "memory: pulled to $(git rev-parse --short HEAD)"
  elif [ "$REMOTE" = "$BASE" ]; then
    git push --quiet && echo "memory: pushed local commits"
  else
    git pull --rebase --autostash --quiet && git push --quiet && echo "memory: rebased + pushed"
  fi
)
```

After this, the submodule working tree is at latest `claude-memory:main`. The parent's pinned SHA may drift — `git add memory` later in Step 6 captures the pointer bump as part of the sync commit.

## Step 2 — Resolve OTHER branch

Machine branches (allowlist). **Edit this list to match your branch naming.** Examples:

```bash
# Replace with your own branch names
ALLOWLIST_REGEX='^(main|laptop|server|desktop)$'
```

```bash
ALL=$(git branch -r | sed 's|^[ *]*origin/||' | grep -vE '^(HEAD|HEAD ->.*)$')
CANDIDATES=$(echo "$ALL" | grep -E "$ALLOWLIST_REGEX" | grep -v "^${CURRENT}$")
```

- 0 candidates → exit with message ("no other machine branch to sync from").
- 1 candidate → auto-pick as `OTHER`.
- ≥2 candidates → use `AskUserQuestion` with one question, options = candidates.

Log: `Syncing FROM origin/${OTHER} INTO ${CURRENT}`.

## Step 3 — Compute change set

```bash
MB=$(git merge-base HEAD origin/${OTHER})

# Files changed on OTHER since merge-base
git diff --name-status ${MB}..origin/${OTHER} > /tmp/cc-sync-other.txt

# Files changed on CURRENT since merge-base
git diff --name-status ${MB}..HEAD > /tmp/cc-sync-current.txt
```

Build per-file status by joining the two lists. For each file path:

| OTHER status | CURRENT status | Action |
|---|---|---|
| A or M | (unchanged) | **fast-forward**: copy OTHER's version in (auto) |
| (unchanged) | A or M | **no-op**: keep CURRENT |
| A or M | A or M | **conflict**: resolve per category (Step 4) |
| D | (unchanged) | **skip** (never auto-delete; log warning) |
| D | M | **skip** (CURRENT keeps its version; log) |
| (any) | (any) but content identical | **no-op** |

Verify "content identical" with `git diff origin/${OTHER}:${path} HEAD:${path} --quiet` before treating as no-op.

## Step 4 — Categorize each path

Apply category by glob match. First match wins.

| Glob | Category |
|---|---|
| `settings.json` | **key-merge** (Step 5) |
| `hooks/**`, `statusline-command.sh`, `channels/**`, `plans/**` | **skip** (unless `--include-machine-config` was passed) |
| `memory` (gitlink) | **submodule-pointer** — already handled in Step 1; treat as no-op here |
| `agent-memory/**/*.md` | **prefer-newer** (Step 4a) — same rules as skills/rules |
| `skills/**`, `rules/**`, `references/**`, `agents/**`, `output-styles/**`, `keybindings.json` | **prefer-newer** (Step 4a) |
| anything else | **skip** (log) |

Note: `memory/**/*.md` (per-context memory entries) is no longer synced via this skill — memory is its own submodule with its own `main` branch. Step 1 already pulled the latest. The cross-branch parent diff for path `memory` is just a gitlink SHA pointer; the working tree is at `claude-memory:main` regardless of which OTHER branch is being synced.

### 4a — prefer-newer (rules, agents, skills, refs, output-styles, keybindings)

For conflicts (modified on both):
```bash
T_OTHER=$(git log -1 --format=%ct origin/${OTHER} -- "${path}")
T_CURR=$(git log -1 --format=%ct HEAD -- "${path}")
```

- `T_OTHER > T_CURR` → apply OTHER (`git checkout origin/${OTHER} -- "${path}"`).
- `T_OTHER ≤ T_CURR` → keep CURRENT (no-op).
- If `|T_OTHER - T_CURR|  /tmp/cc-sync-other-settings.json
jq -s '
  .[0] as $cur | .[1] as $oth |
  $cur
  | .env = ($oth.env // {}) + ($cur.env // {})
  | .permissions.allow = (((($cur.permissions.allow // []) + ($oth.permissions.allow // [])) | unique))
  | .permissions.deny  = (((($cur.permissions.deny  // []) + ($oth.permissions.deny  // [])) | unique))
  | .hooks = (($oth.hooks // {}) * ($cur.hooks // {}))
  | .enabledPlugins = (($oth.enabledPlugins // {}) * ($cur.enabledPlugins // {}))
' settings.json /tmp/cc-sync-other-settings.json > /tmp/cc-sync-settings-merged.json
```

(Note: `jq`'s `*` operator does recursive merge with right-hand precedence — that's why CURRENT is on the right for `hooks` and `enabledPlugins`.)

If the merged result is byte-equal to CURRENT's `settings.json` → no-op. Else write it and stage.

Show a summary of which keys changed (added/modified) before applying.

## Step 6 — Apply, commit, push

For each path with action `fast-forward` or `apply OTHER`:
```bash
git checkout origin/${OTHER} -- "${path}"
```

For `settings.json` — write merged content directly via Write tool.

After all changes applied:
```bash
git add -A
COUNT=$(git diff --cached --name-only | wc -l | tr -d ' ')
git commit -m "chore(${CURRENT}): sync from ${OTHER} — ${COUNT} files"
git push
```

`git add -A` will pick up the `memory` gitlink change if Step 1 advanced the submodule pointer. The pointer bump is recorded as part of this same sync commit — no separate commit needed.

If `COUNT=0` → no commit, just print "Nothing to sync".

### Step 6.5 — Rebuild nf-dream Go binary if source changed

`skills/nf-dream/scripts/go/` houses the Go source for the nf-dream binary. The compiled binary at `skills/nf-dream/scripts/bin/-/nf-dream` is gitignored (per-machine build). If the sync pulled in Go source changes, the binary on this machine is stale until rebuilt.

```bash
GO_DIR="$HOME/.claude/skills/nf-dream/scripts/go"
BIN_DIR="$HOME/.claude/skills/nf-dream/scripts/bin"
case "$(uname -s)-$(uname -m)" in
  Darwin-arm64)        target="darwin-arm64" ;;
  Linux-x86_64)        target="linux-amd64" ;;
  Linux-aarch64|Linux-arm64) target="linux-arm64" ;;
  *)                   target="" ;;
esac
BIN="$BIN_DIR/$target/nf-dream"

if [[ -n "$target" && -d "$GO_DIR" ]]; then
  newest_src=$(find "$GO_DIR" -name '*.go' -newer "$BIN" 2>/dev/null | head -1)
  if [[ ! -x "$BIN" || -n "$newest_src" ]]; then
    echo "nf-cc-sync: rebuilding nf-dream Go binary (source changed)"
    bash "$HOME/.claude/skills/nf-dream/scripts/build.sh"
  fi
fi
```

Diagnose any shim's runtime decision after sync: `~/.claude/skills/nf-dream/scripts/ --runtime-report`.

## Step 7 — Final report

Print a structured summary:

```
nf-cc-sync: ${CURRENT} ← origin/${OTHER}
  Pre-flight:    [auto-committed N dirty files | clean]
  Memory:        [up to date  | pulled to  | pushed | rebased + pushed]
  Fast-forward:  N files
  Conflicts:     N (auto-resolved by newer-wins)  / N (asked user)
  Skipped:       N (machine-config policy) / N (deletes — never auto-delete)
  Settings.json: keys changed: [env, permissions.allow, ...]
  Pointer bump:  memory  →  | (no change)
  Commit:         "chore(${CURRENT}): sync from ${OTHER} — N files"
  Pushed:        yes
```

## Auto-as-much-as-possible defaults

- Single OTHER candidate → no question, auto-pick.
- Conflict with >24h gap → no question, newer wins.
- Conflict within 24h → for rules/skills/etc. default to newer-wins silently and note in report. (Memory is no longer synced via this skill — it's a submodule, see Step 1.)
- Settings.json with no key-level conflicts → no question, apply.
- Settings.json with new permissions.deny entries → silently union (deny is always safe to add).

## Invocation

- `/nf-cc-sync` — run with default policy (skip machine-config).
- `/nf-cc-sync --include-machine-config` — also sync `hooks/`, `statusline-command.sh`, `channels/**`. Use sparingly (these may have OS-specific behavior).
- `/nf-cc-sync --dry-run` — compute the plan and print Step 7 report without applying any changes or commits.

## Out of scope

- Pushing CURRENT's changes to OTHER (forbidden — pull-only).
- Editing `.gitignored` files (`.local/`, `settings.local.json`, `projects/`, etc.).
- Per-context memory file merge — handled by the `memory/` submodule's own `main` branch (Step 1). This skill only bumps the parent's pinned SHA pointer.
- Adding new machine branches automatically — you edit the allowlist in Step 2 by hand.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [llawliet11](https://github.com/llawliet11)
- **Source:** [llawliet11/claude-skills-toolkit](https://github.com/llawliet11/claude-skills-toolkit)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-llawliet11-claude-skills-toolkit-nf-cc-sync
- Seller: https://agentstack.voostack.com/s/llawliet11
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
