From 3ec99142f3f9aac03e2a57d3f825f8fd3c8a36b3 Mon Sep 17 00:00:00 2001 From: Doll Date: Mon, 11 May 2026 14:04:54 -0500 Subject: [PATCH] fix(kickoff): stop --force-reinitting hook-config.json on every worktree + container start (#732, GH#583) Both the host-side `init_worktree_agent` (in `kickoff/launch.rs`) and the container entrypoint (`entrypoint.sh`) ran `crosslink init --force` on every kickoff, unconditionally re-templating `.crosslink/hook-config.json` from the embedded default. That left a ~50-line diff in every agent-produced worktree -- top-level keys moving into `agent_overrides`, new `agent_lint_commands` / `agent_test_commands` fields appearing -- which leaked into every kickoff PR and required reviewers to remember to `git restore .crosslink/hook-config.json` before merging. ## Fix Drop `--force` from both call sites. Plain `crosslink init` short-circuits at `commands/init/mod.rs:751` when `.crosslink/` and `.claude/` already exist (the common case for any worktree checked out from a branch that committed both). That makes the init a deliberate no-op on existing state instead of an unconditional re-template. - `crosslink/resources/container/entrypoint.sh` -- container's init call. - `crosslink/src/commands/kickoff/launch.rs::init_worktree_agent` -- host-side init that runs against the freshly-created worktree before the container starts. The user-facing symptom required both fixes: the host call re-templates first, then the container call re-templates again, so dropping `--force` from only one wouldn't make the diff go away. Kept `--skip-signing` and `--defaults` on the host call because they suppress the TUI walkthrough on the rare path where init actually has work to do (`.crosslink/` or `.claude/` missing). When users genuinely want to refresh hooks they still run `crosslink init --force` manually -- the existing documented path. ## Verification - `cargo fmt --check` clean - `cargo clippy --lib --bin crosslink -- -D warnings` clean - `cargo test --bin crosslink kickoff::` 149/149 pass - `bash -n entrypoint.sh` clean Co-Authored-By: Claude Opus 4.7 (1M context) --- crosslink/resources/container/entrypoint.sh | 9 +++++++-- crosslink/src/commands/kickoff/launch.rs | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crosslink/resources/container/entrypoint.sh b/crosslink/resources/container/entrypoint.sh index f8cf43f4..888b050a 100644 --- a/crosslink/resources/container/entrypoint.sh +++ b/crosslink/resources/container/entrypoint.sh @@ -125,10 +125,15 @@ fi # --- Crosslink init --- # Set up hooks, skills, and policy in the workspace so container agents are -# bound by the same rules as host agents. +# bound by the same rules as host agents. Plain `init` (no --force) is +# idempotent: it short-circuits when `.crosslink/` and `.claude/` already +# exist in the worktree (the common case, since both are git-committed and +# arrive with the worktree checkout). This is what prevents the entrypoint +# from re-templating `hook-config.json` on every container start and leaking +# spurious diffs into agent PRs. See GH#583. if [ -n "$WORKSPACE" ] && command -v crosslink &>/dev/null; then echo "[crosslink-entrypoint] Initializing crosslink hooks in workspace..." - gosu agent bash -c "cd '$WORKSPACE' && crosslink init --force" 2>&1 || true + gosu agent bash -c "cd '$WORKSPACE' && crosslink init" 2>&1 || true fi echo "[crosslink-entrypoint] Setup complete. Running command as agent..." diff --git a/crosslink/src/commands/kickoff/launch.rs b/crosslink/src/commands/kickoff/launch.rs index e175dc1d..19d1244c 100644 --- a/crosslink/src/commands/kickoff/launch.rs +++ b/crosslink/src/commands/kickoff/launch.rs @@ -433,10 +433,17 @@ pub(super) fn init_worktree_agent( crosslink_dir: &Path, compact_name: &str, ) -> Result { - // Run crosslink init --force in the worktree + // Run `crosslink init` in the worktree. Plain init (no --force) is + // idempotent: it short-circuits when `.crosslink/` and `.claude/` already + // exist, which is the common case for a worktree freshly checked out + // from a branch that has both committed. We keep `--skip-signing` and + // `--defaults` to suppress the TUI walkthrough on the rare path where + // init actually has work to do. Dropping `--force` here prevents the + // worktree's `hook-config.json` from being re-templated and leaking a + // spurious diff into every agent-produced PR. See GH#583. let output = Command::new("crosslink") .current_dir(worktree_dir) - .args(["init", "--force", "--skip-signing", "--defaults"]) + .args(["init", "--skip-signing", "--defaults"]) .output() .context("Failed to run crosslink init in worktree")?;