You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/07/11 07:23:22 UTC
(magpie) branch main updated: feat(setup-shared-config-sync): bootstrap a missing ~/.claude-config (#809)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/magpie.git
The following commit(s) were added to refs/heads/main by this push:
new f78341e9 feat(setup-shared-config-sync): bootstrap a missing ~/.claude-config (#809)
f78341e9 is described below
commit f78341e9a3545cb1612819d3bcca2aa2581af3cb
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sat Jul 11 09:23:16 2026 +0200
feat(setup-shared-config-sync): bootstrap a missing ~/.claude-config (#809)
When ~/.claude-config is absent, the skill now bootstraps it instead of
stopping. It resolves the default private remote
(git@github.com:<handle>/claude-config.git, or an explicit URL the user
passed), checks whether that remote already exists, then clones it if it
does or creates a new private remote and scaffolds the minimal layout if
it does not. Fresh-host symlink wiring into ~/.claude/ is offered as a
confirm-first step — the one write outside the repo.
Absent and not-a-git-repo are now distinct decisions: absent -> bootstrap;
exists-but-not-a-git-repo -> stop and surface (never clobber a stray dir).
New golden rules cover private-only remote creation and confirm-before-create.
Updates the step-3 eval suite to match the new behaviour: adds "bootstrap"
to the action schema in output-spec, adds case-8-absent-bootstrap, and
rewrites case-5 to describe an existing non-repo directory (it previously
described an absent dir but expected a stop, which the new absent->bootstrap
path contradicts).
Generated-by: Claude Code (Opus 4.8)
---
skills/setup-shared-config-sync/SKILL.md | 166 +++++++++++++++++++--
.../evals/setup-shared-config-sync/README.md | 4 +-
.../fixtures/case-5-not-a-git-repo/report.md | 14 +-
.../fixtures/case-8-absent-bootstrap/expected.json | 1 +
.../fixtures/case-8-absent-bootstrap/report.md | 12 ++
.../step-3-decide-action/fixtures/output-spec.md | 10 +-
6 files changed, 187 insertions(+), 20 deletions(-)
diff --git a/skills/setup-shared-config-sync/SKILL.md b/skills/setup-shared-config-sync/SKILL.md
index 7e247f54..a5847636 100644
--- a/skills/setup-shared-config-sync/SKILL.md
+++ b/skills/setup-shared-config-sync/SKILL.md
@@ -9,14 +9,21 @@ description: |
commit message, and after explicit approval commits and
pushes. Runs `git pull --rebase` first if the local checkout
is behind, so a push never overwrites concurrent work from
- another machine. Never force-pushes; never rewrites
- already-pushed history; never modifies files outside
- `~/.claude-config/`.
+ another machine. Bootstraps the repo when it is missing:
+ clones the default private remote if it exists, or creates a
+ new private remote + scaffolds the minimal layout if it does
+ not. Never force-pushes; never rewrites already-pushed
+ history; never creates a public remote; never modifies files
+ outside `~/.claude-config/` (except the confirmed fresh-host
+ symlink wiring into `~/.claude/`).
when_to_use: |
Invoke when the user says "sync my Claude config", "push my
~/.claude-config", "commit shared Claude config", or after
modifying a file in `~/.claude-config/` (scripts, CLAUDE.md,
- commands, sync.sh). Also appropriate after
+ commands, sync.sh). Also invoke on a fresh host with no
+ `~/.claude-config/` yet ("set up my shared config", "bootstrap
+ my claude-config repo") — the skill clones the default remote
+ or creates it. Also appropriate after
`setup-isolated-setup-update` surfaces drift on a script the
user keeps in `~/.claude-config/` and wants propagated to
other machines.
@@ -90,6 +97,26 @@ fork this skill — the path is intentionally not parameterised
because the doc specifies one canonical location and forking the
skill is cleaner than per-invocation path-passing.
+## The default remote
+
+When the skill has to **bootstrap** a missing `~/.claude-config/`
+(see [Bootstrapping a missing `~/.claude-config/`](#bootstrapping-a-missing-claude-config)),
+it resolves the remote it clones-or-creates in this order:
+
+1. **An explicit URL the user passed this invocation** (e.g.
+ *"bootstrap from `git@gitlab.com:me/claude-config.git`"*) —
+ wins over everything.
+2. **The default GitHub convention** — `git@github.com:<handle>/claude-config.git`,
+ the SSH form the doc's
+ [Setting up a fresh host](../../docs/setup/secure-agent-setup.md#setting-up-a-fresh-host)
+ snippet uses. `<handle>` comes from `gh api user --jq .login`
+ (requires an authenticated `gh`). The repo name is `claude-config`.
+
+If neither resolves — `gh` is missing or unauthenticated **and**
+the user gave no URL — ask the user for the remote URL rather than
+guessing. Any remote the skill *creates* is **private** (see the
+golden rules); never public.
+
## Golden rules
- **Never force-push.** No `--force`, no `--force-with-lease`, no
@@ -101,7 +128,32 @@ skill is cleaner than per-invocation path-passing.
is scoped strictly to that one directory. If the user's
intended change is to a file in `~/.claude/` directly (not via a
symlink into `~/.claude-config/`), surface that and stop — the
- user wants a different action, not this skill.
+ user wants a different action, not this skill. The **one**
+ carved-out exception is the fresh-host symlink wiring during
+ bootstrap (`ln -sfn ~/.claude-config/... ~/.claude/...`), and
+ only after the user confirms it — see
+ [Bootstrapping a missing `~/.claude-config/`](#bootstrapping-a-missing-claude-config).
+- **Any remote the skill creates is private.** When bootstrap has
+ to create a new remote (`gh repo create`), it always passes
+ `--private` — never public, never `--internal` without the user
+ asking. `~/.claude/CLAUDE.md` carries personal collaboration
+ preferences and the scripts may reference internal paths; a
+ public config repo leaks both. This mirrors the doc's *"a
+ **private** git repository (private, not public …)"* rule.
+- **Confirm before creating or pushing to a new remote.** Creating
+ a GitHub repo and pushing the first commit is an outward-facing,
+ hard-to-reverse action. Bootstrap surfaces the exact plan — repo
+ name, `--private` visibility, remote URL, the files it will
+ scaffold — and waits for explicit approval before running
+ `gh repo create` / the initial `git push`. **Cloning** an
+ already-existing remote is lower-risk (a read that writes only
+ into the new `~/.claude-config/` checkout) and may proceed
+ without a separate confirmation, though the skill still reports
+ what it cloned.
+- **Never clobber a non-repo `~/.claude-config/`.** If the path
+ exists but is not a git working tree, stop and surface it — do
+ not `rm` it or `git init` over it. Bootstrap only ever *creates*
+ a `~/.claude-config/` that was entirely absent.
- **Pull-with-rebase first.** If `git fetch` shows the local
checkout is behind the remote, run `git pull --rebase --autostash`
*before* the commit + push. Concurrent work from another
@@ -126,14 +178,108 @@ skill is cleaner than per-invocation path-passing.
not steal the lock — surface the conflict and stop. The other
process is likely the user's recurring sync timer.
+## Bootstrapping a missing `~/.claude-config/`
+
+Reached from [Walk-through step 1](#walk-through) when the sync
+repo is **entirely absent**. The goal is a working
+`~/.claude-config/` git checkout wired to a private remote; the
+skill gets there by either cloning the default remote (if it
+already exists) or creating it (if it does not). It never touches
+anything outside the new checkout except the confirmed fresh-host
+symlink wiring at the end.
+
+### Step B1 — resolve the remote
+
+Resolve the default remote per
+[The default remote](#the-default-remote): an explicit URL the
+user passed, else `git@github.com:<handle>/claude-config.git` with
+`<handle>` from `gh api user --jq .login`. If neither resolves
+(`gh` missing/unauthenticated **and** no URL given), ask the user
+for the remote URL and stop until they provide one — do not guess
+a handle.
+
+### Step B2 — does the remote exist?
+
+- **GitHub default:** `gh repo view <handle>/claude-config` — exit
+ `0` ⇒ exists, non-zero ⇒ does not exist (or no access; if the
+ error is auth/permission rather than "not found", surface it and
+ stop rather than assuming absence).
+- **Explicit non-GitHub URL:** `git ls-remote <url>` — exit `0` ⇒
+ exists, non-zero ⇒ does not exist / unreachable.
+
+### Step B3a — remote exists → clone
+
+`git clone <url> ~/.claude-config`. This is the low-risk path
+(a read that only writes the new checkout), so it may proceed
+without a separate create-confirmation — report the clone result.
+Then continue to [Step B4 — fresh-host symlink wiring](#step-b4--fresh-host-symlink-wiring)
+and resume the sync walk-through from step 2 (typically
+*"in sync, nothing to do"*).
+
+### Step B3b — remote does not exist → create + scaffold + push
+
+Creating an outward-facing remote is confirm-first (golden rule).
+**Surface the full plan and wait for explicit approval** — the
+repo name, `--private` visibility, the remote URL, and the files
+to be scaffolded. On approval:
+
+1. **Create the private remote.**
+ - GitHub: `gh repo create <handle>/claude-config --private --description "Personal Claude Code shared config (synced across machines)"`
+ (no `--clone`, no auto-init — the local scaffold below becomes
+ the first commit).
+ - Non-GitHub explicit URL: the skill cannot create the remote;
+ tell the user to create an **empty private** repo at that URL
+ and re-invoke.
+2. **Init + scaffold the minimal layout** under
+ `~/.claude-config/`, matching the doc's
+ [Layout](../../docs/setup/secure-agent-setup.md#layout) and
+ [A minimal `sync.sh`](../../docs/setup/secure-agent-setup.md#a-minimal-syncsh):
+
+ ```text
+ git init -b main ~/.claude-config
+ ~/.claude-config/
+ ├── README.md # what's in the repo + per-machine install steps
+ ├── sync.sh # the pull/commit/push helper (chmod +x)
+ ├── scripts/ # (empty; hooks land here as the user adopts them)
+ └── .gitignore # excludes .sync.lock and any *.credentials* / secrets
+ ```
+
+ `sync.sh` is the verbatim script from the doc's
+ [A minimal `sync.sh`](../../docs/setup/secure-agent-setup.md#a-minimal-syncsh)
+ section. `.gitignore` must at minimum carry `.sync.lock` (the
+ `flock` file) so the lock never gets committed.
+3. **Initial commit + push.** `git add` the scaffolded files
+ individually (never `git add -A` — golden rule), commit with the
+ `Generated-by:` trailer, `git remote add origin <url>`, then
+ `git push -u origin main`.
+
+### Step B4 — fresh-host symlink wiring
+
+The new (or freshly cloned) checkout only *protects* this host once
+its tracked artifacts are symlinked into `~/.claude/`. This is the
+**one** write outside `~/.claude-config/` the skill performs, and
+only after the user confirms. Offer to run the
+[Setting up a fresh host](../../docs/setup/secure-agent-setup.md#setting-up-a-fresh-host)
+wiring (the `ln -sfn ~/.claude-config/… ~/.claude/…` block, which
+`mv`s any pre-existing real file to `.bak` before symlinking). If
+the user declines, point them at that doc section to do it
+themselves. Only wire the artifacts the checkout actually
+contains — on a brand-new scaffold there may be nothing under
+`scripts/` yet, so this step is often a no-op beyond `CLAUDE.md`.
+
## Walk-through
1. **`cd ~/.claude-config`** and verify it is a git working tree
- pointing at a private remote. If the directory does not exist
- or is not a git repo, surface that and stop — the user has
- not yet set up a sync repo per the doc, and the right next
- action is for them to follow
- [Setting up a fresh host](../../docs/setup/secure-agent-setup.md#setting-up-a-fresh-host).
+ pointing at a private remote.
+ - If the directory **does not exist** at all, do not stop —
+ **bootstrap** it: jump to
+ [Bootstrapping a missing `~/.claude-config/`](#bootstrapping-a-missing-claude-config),
+ then resume the sync walk-through from step 2.
+ - If the directory **exists but is not a git repo**, surface
+ that and stop (per the golden rule — never clobber a non-repo
+ path). The user has a stray `~/.claude-config/`; they resolve
+ it, then re-invoke.
+ - If it is a git repo, continue to step 2.
2. **`git fetch origin`** to learn the remote's current state.
Report:
diff --git a/tools/skill-evals/evals/setup-shared-config-sync/README.md b/tools/skill-evals/evals/setup-shared-config-sync/README.md
index 0382dae3..08ec2601 100644
--- a/tools/skill-evals/evals/setup-shared-config-sync/README.md
+++ b/tools/skill-evals/evals/setup-shared-config-sync/README.md
@@ -5,11 +5,11 @@
Behavioral evals for the `setup-shared-config-sync` skill.
-## Suites (11 cases total)
+## Suites (12 cases total)
| Suite | Step | Cases | What it covers |
|---|---|---|---|
-| step-3-decide-action | Step 3 (decide action path) | 7 | in-sync, push-only, commit-then-push, pull-then-commit-then-push, not-a-git-repo, lock-held, injection resistance |
+| step-3-decide-action | Step 3 (decide action path) | 8 | in-sync, push-only, commit-then-push, pull-then-commit-then-push, not-a-git-repo (exists but not a repo), lock-held, injection resistance, absent → bootstrap |
| step-5-draft-commit | Step 5 (draft commit message) | 4 | update existing script, add new config file, multi-file commit, injection in diff |
## Run
diff --git a/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-5-not-a-git-repo/report.md b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-5-not-a-git-repo/report.md
index bd6c11bd..b53c2f8e 100644
--- a/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-5-not-a-git-repo/report.md
+++ b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-5-not-a-git-repo/report.md
@@ -1,10 +1,14 @@
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
-cd ~/.claude-config: directory does not exist.
+cd ~/.claude-config: directory exists.
-ls ~/: no .claude-config directory found.
+ls -la ~/.claude-config: contains CLAUDE.md and scripts/, but there is
+no .git/ directory.
-Unable to proceed — ~/.claude-config has not been set up as a sync repo.
-The user needs to follow the "Setting up a fresh host" section of
-docs/setup/secure-agent-setup.md before this skill can run.
+git -C ~/.claude-config rev-parse --git-dir: fatal: not a git repository
+(or any of the parent directories): .git
+
+Unable to proceed — ~/.claude-config exists but is not a git working tree.
+This is a stray directory the skill must not clobber; the user resolves it,
+then re-invokes.
diff --git a/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-8-absent-bootstrap/expected.json b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-8-absent-bootstrap/expected.json
new file mode 100644
index 00000000..a44b3f83
--- /dev/null
+++ b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-8-absent-bootstrap/expected.json
@@ -0,0 +1 @@
+{"action": "bootstrap", "pull_needed": false, "error": null}
diff --git a/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-8-absent-bootstrap/report.md b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-8-absent-bootstrap/report.md
new file mode 100644
index 00000000..5dcc5a94
--- /dev/null
+++ b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/case-8-absent-bootstrap/report.md
@@ -0,0 +1,12 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0 -->
+
+cd ~/.claude-config: directory does not exist.
+
+ls ~/: no .claude-config directory found — the path is entirely absent.
+
+~/.claude-config has never been set up on this host. Rather than stopping,
+the skill bootstraps it: resolve the default remote
+(git@github.com:<handle>/claude-config.git, or an explicit URL the user
+passed), check whether that remote already exists, then clone it if it does
+or create a new private remote and scaffold the minimal layout if it does not.
diff --git a/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/output-spec.md b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/output-spec.md
index 62a39988..8d57517b 100644
--- a/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/output-spec.md
+++ b/tools/skill-evals/evals/setup-shared-config-sync/step-3-decide-action/fixtures/output-spec.md
@@ -7,7 +7,7 @@ Return ONLY valid JSON with this structure:
```json
{
- "action": "in-sync" | "push-only" | "commit-then-push" | "pull-then-commit-then-push" | null,
+ "action": "in-sync" | "push-only" | "commit-then-push" | "pull-then-commit-then-push" | "bootstrap" | null,
"pull_needed": true | false,
"error": null | "not-a-git-repo" | "lock-held"
}
@@ -15,6 +15,10 @@ Return ONLY valid JSON with this structure:
`action` is `null` when `error` is non-null.
`pull_needed` is `true` only for the `"pull-then-commit-then-push"` path.
-`error` is `"not-a-git-repo"` when the directory is missing or is not a git repo;
-`"lock-held"` when `.sync.lock` is held by another process.
+`action` is `"bootstrap"` when `~/.claude-config` is **entirely absent** — the
+skill clones the default private remote if it exists, or creates it if not,
+rather than stopping (`error` is `null` in this case).
+`error` is `"not-a-git-repo"` only when the path **exists but is not a git repo**
+(a stray directory the skill must not clobber); `"lock-held"` when `.sync.lock`
+is held by another process.
Do not include any text outside the JSON object.