Gated Dual-Clone Workflow
One-liner · 一句话
EN — Clone the upstream twice: a gateway repo that pushes, and a
satellite repo that builds. The satellite's origin points at the local
gateway path — so a stray git push from the compile tree is physically
unable to reach the remote.
ZH — 把上游仓库 clone 两份:一个 gateway 仓库负责 push,一个
satellite 仓库只读编译。satellite 的 origin 指向本地 gateway 路径,
编译树里误跑一条 git push 在物理上也够不到远程。
Should I use this? · 该不该用
→ See references/decision-checklist.md
Four yes/no questions. Three or more "yes" → use this skill. Otherwise fall
back to a single repo or git worktree.
四道是非题:≥ 3 个 yes 用本 skill,否则单仓库或 git worktree 更合适。
Minimal bootstrap · 最小搭建
scripts/bootstrap.sh \
--remote git@gitlab.example.com:team/project.git \
--upstream-branch release \
--push-branch feature/alice-auth \
--gateway-dir ~/projects/foo-work \
--satellite-dir ~/projects/foo-verify \
--user-email alice@example.com \
--user-name alice
All 7 flags are required. --dry-run prints the command sequence without
executing. --force is only valid for empty target dirs (non-empty is always
rejected; clean up by hand first). See scripts/bootstrap.sh --help for
detail.
Post-setup · 搭建完成后(3 道检查)
bootstrap.sh verifies three safety layers before exiting 0:
- Gate A · Protocol wall —
cd satellite && git pushrejected ("DISABLED" in error). Satellite'soriginis a local path; no network route to upstream exists. - Gate B · Explicit disable — Satellite's push URL is literally set to
DISABLED, so even malformed config still errors fast instead of silently trying. - Gate C · Pre-push hook — Gateway's
pre-pushhook rejects any push to a protected-branch regex (default: the upstream branch passed in).
Gate failures halt bootstrap with a clear reason — nothing is left in a partial state that the user has to hand-clean.
三道检查都过才算 bootstrap 成功。任一失败会明确说明原因,不留一半成品。
Daily operations · 日常操作
→ See references/daily-workflow.md
The core loop: edit in gateway, sync satellite with scripts/sync-satellite.sh,
build in satellite, push from gateway, open MR/PR. Four commands, copy-paste
ready.
日常 4 步:在 gateway 里改 → sync-satellite.sh 同步到 satellite →
satellite 里编译 → gateway 里 git push → GitLab / GitHub 上发 MR/PR。
cheatsheet 里每步都是现成命令。
Safety model · 安全模型
Three layers, each independent. Any one holds; all three together is belt, braces, and shoulder strap.
- Protocol layer — satellite's
originis a file path; no TCP/SSH route to upstream exists from the compile tree. - Config layer — satellite's push URL = literal
DISABLED. Explicit failure mode. - Hook layer — gateway's
pre-pushhook rejects protected-branch refs.
Critical caveat · 关键注意: Client-side git hooks are advisory, not
enforcement. --no-verify bypasses them. Real protection = server-side
protected-branch rules in GitLab / GitHub / Gerrit. This skill's hook is a
second layer — catches finger-slips, not determined bypass. Enable
server-side branch protection alongside.
客户端 hook 只是提醒,不是强制边界。--no-verify 可以绕过。真正的保护
在托管平台的 protected-branch 规则上。本 skill 的 hook 是第二层,接手滑,
不防恶意。服务端保护一起开。
Variants · 变体
- 1 gateway + N satellites — run
bootstrap.shagain with--skip-gateway+ a new--satellite-dir. Useful forverify/debug/testtrees. - 3-clone with reproducibility gate (see below) — add a
clean-verifyclone on separate disk/machine, with a pre-push hook that refuses to push anything clean-verify hasn't stamped. Trade extra disk + extra build time for catching "works on SSD, fails on HDD / CI" bugs before they reach upstream. - Worktree fallback — if disk is tight (< 2× source size) and single
developer, single machine:
git worktreetrades isolation for disk. Seedecision-checklist.mdfor when worktree beats this skill. - Hosting differences — GitLab MR, GitHub PR, Gerrit push-to-
refs/for/*. The skill is host-neutral; protected-branch regex is a per-project flag.
Optional 3rd clone · reproducibility gate · 可选 第 3 仓 · reproducibility 关卡
For projects where a failed build on CI (different filesystem, different disk speed, different filesystem cache state) has a real cost — add a clean-verify clone on a separate disk (or separate machine) and gate every push on a from-scratch full-build in it.
When to use (all four should be true):
- Daily dev happens on SSD; CI / shipping happens on HDD or remote machine.
- The project has reproducibility bugs that only surface from a cold, clean tree (SSD cache / generated files / stale build artefacts hide them on the iteration clone).
- The push path is high-stakes (production branch, signed release, audit trail required).
- The team already has discipline to run a pre-push command.
Topology:
┌──────────┐ ┌──────────┐ ┌───────────────────┐
upstream ───► │ gateway │ ────► │satellite │ │ clean-verify │
(push only │ (dev + │ sync │ (build · │ │ (pre-push gate · │
from gateway) │ push) │ via │ disabled│ │ HDD / diff disk)│
│ SSD │ local │ push) │ │ │
└──▲───────┘ path └──────────┘ └───▲───────────────┘
│ │
│ pre-push hook reads .git/last-clean-verify
│ and refuses to push a commit whose sha ≠ stamped
│ │
└─── clean-verify-run.sh stamps on success ─┘
Bootstrap it:
scripts/bootstrap.sh \
--remote git@gitlab.example.com:team/project.git \
--upstream-branch release \
--push-branch feature/alice-auth \
--gateway-dir ~/projects/foo-work \
--satellite-dir ~/projects/foo-verify \
--clean-verify-dir /mnt/hdd/foo-clean-verify \
--user-email alice@example.com \
--user-name alice
Same as the 2-clone bootstrap, plus --clean-verify-dir=<path>. The
script adds:
- Step 5b —
git clonethe clean-verify from gateway (same local-path trick as satellite · origin on a local path, never the real remote). - Step 6b — set
pushurl = DISABLEDon bothoriginand the diagnosticupstreamremote. Check out<push-branch>. - Gate D — in Step 7 post-setup, verify clean-verify push is DISABLED.
- Pre-push hook gets a 2nd gate —
install-hooks.shis invoked with--enforce-clean-verify, which adds a "stamp match" check alongside the protected-branch check.
Daily flow (5 steps instead of 4):
- Edit in gateway, commit.
sync-satellite.sh→ satellite → build + test there (fast iteration).- When ready to push, run:
This syncs clean-verify from gateway, runsscripts/clean-verify-run.sh \ --gateway-dir=<gw> --clean-verify-dir=<cv> \ --push-branch=<br> --build-cmd='<full-build command>' --yesgit clean -fdx(drops every untracked/ignored file), runs your full build end-to-end, and on success writesgateway/.git/last-clean-verify. git push origin <push-branch>— the hook reads the stamp, refuses if commit doesn't match. Emergency bypass:git push --push-option=allow-unverified(use sparingly; the point of the gate is that it holds).- Raise MR/PR.
What this catches that 2-clone doesn't:
- SSD-only bugs: code depends on files that happen to be in OS filesystem cache on the dev SSD but not on a cold HDD.
- Stale-artefact bugs: satellite has build products from an earlier commit
that accidentally satisfy a missing
#include/ missing codegen that the current commit wouldn't produce fresh. - Dirty-tree bugs: satellite has hand-edits the author forgot about; they pass the build; clean-verify starts from git HEAD and fails.
- Build-command drift:
--build-cmdis sha256-hashed into the stamp; if team changes build command out from under you, next push asks for a re-verify.
Paired evaluator: gated-dual-clone-audit auto-detects 3-clone mode
when you pass --clean-verify-dir, and runs 4 extra gates (S9-S11 + C9 +
B4) to re-verify the topology on demand.
Files · 文件
scripts/bootstrap.sh— the main setup command, runs all 8 steps + 3 gates (4 gates in 3-clone mode) · accepts optional--clean-verify-dirscripts/sync-satellite.sh— fetch / reset-hard / merge-ff modes for syncing satellite from gatewayscripts/clean-verify-run.sh— sync + clean + full-build + stamp · the pre-push reproducibility gate for 3-clone modescripts/install-hooks.sh— writes thepre-pushhook on the gateway ·--enforce-clean-verifyadds the stamp-match gatescripts/check_before_mr.sh— 5 checks to run right before opening the MR (clean tree · clean-verify repo exists · stamp present · stamp sha == branch head · remote == local). For push-early / MR-late projects — see belowreferences/decision-checklist.md— when to use / when not to usereferences/daily-workflow.md— 4-step cheatsheetreferences/patterns.md— N-satellite / worktree fallback / GitHub-GitLab-Gerrit shapesreferences/guardrails.md— why the three gates work / when they break / how to verifyreferences/server-side-enforcement.md— GitLab / GitHub / Gerrit / pre-receive / Actions templates (real enforcement beyond advisory client-side hook)references/troubleshooting.md— 10 common failures, symptom → diagnose → fix → preventreferences/comparison.md— dual-clone vs worktree vs single-repo, 11-dimension table
Push-early / MR-late · 先推分支,最后才开 MR
有些项目这样走:个人分支随时 push 到远端当备份,一整个主题做完才开 MR。
这时 install-hooks.sh --enforce-clean-verify 那道会碍事 —— 它不看分支名,
任何 push 都拦,于是每次备份性质的 push 都要 --push-option=allow-unverified。
绕过一旦变成肌肉记忆,哪天真该拦的那次也会顺手绕过 —— 比关掉更糟, 因为还以为有人看着。
而真正该卡的是开 MR 那一刻,那是托管平台上的动作,不是 git 事件,pre-push hook 根本接不到。所以这类项目应该:
# 1. 建仓时【不加】 --enforce-clean-verify(或事后把 hook 里 enforce_cv 改 0)
# Gate 1(保护分支)照旧生效,个人分支随便推
# 2. 开 MR 之前跑这个,五项全过才去点开 MR
scripts/check_before_mr.sh \
--gateway-dir=<gateway> \
--clean-verify-dir=<clean-verify> \
--branch=<待 MR 的分支>
五项:
| 查什么 | 漏了会怎样 | |
|---|---|---|
| M1 | gateway 工作区干净 | 要 MR 的不是手上这份 |
| M2 | clean-verify 仓在位且是 git 仓 | 空目录 = 那道关卡从来没建起来过 |
| M3 | 盖章文件存在 | 压根没跑过 clean-verify-run.sh |
| M4 | 盖章 sha == 待 MR 分支 HEAD | 跑过,但之后又提交了几笔 —— 最容易漏的一种 |
| M5 | 远端分支 == 本地 HEAD | 本地验过没推,MR 提的是远端那份旧的 |
怎么选:每次 push 都该验 → 用 --enforce-clean-verify;
先推后 MR → 关掉它,改用 check_before_mr.sh。两个都不用 = 那道关卡不存在。
Reference · 参考
- Anthropic · harness design for long-running apps
- Full design spec:
docs/design-mr-gated-dual-repo.md - Live demo:
demos/gated-dual-clone/index.html