From 15adc7afa5449ad133ca6afb30c0bd61a1ad1f7d Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 12 Aug 2026 10:02:13 -0700 Subject: [PATCH] fix(ship): custom-hooks-path detection false-negatives on git worktrees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-push guard's HOOKS_IN_GIT_DIR check compared the hooks dir against --absolute-git-dir, which in a linked worktree is .git/worktrees/ while hooks resolve to the COMMON .git/hooks — so every Conductor worktree read as a 'custom hooks path' and the consented guard install was skipped. Match against the resolved --git-common-dir too (with a /nonexistent fallback so a failed resolution can't collapse the case pattern into match-everything). Verified live: this worktree now reports yes (was no), and the main checkout still reports yes. Goldens refreshed (--host all). Co-Authored-By: Claude Fable 5 --- ship/SKILL.md | 8 +++++++- ship/SKILL.md.tmpl | 8 +++++++- test/fixtures/golden/claude-ship-SKILL.md | 8 +++++++- test/fixtures/golden/codex-ship-SKILL.md | 8 +++++++- test/fixtures/golden/factory-ship-SKILL.md | 8 +++++++- 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/ship/SKILL.md b/ship/SKILL.md index c86722534..4730797b0 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -1272,9 +1272,15 @@ _HOOK_INSTALLED="no" # committed hook and write a machine-local wrapper into the working tree. _HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") _GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +# Linked worktrees: --absolute-git-dir is .git/worktrees/ but hooks +# resolve to the COMMON .git/hooks, so match against the common dir too or +# every Conductor worktree false-negatives as a "custom hooks path". The +# /nonexistent fallback keeps the case pattern from collapsing to "/*" +# (match-everything) when resolution fails. +_GIT_COMMON=$(cd "$(git rev-parse --git-common-dir 2>/dev/null || echo /nonexistent)" 2>/dev/null && pwd || echo /nonexistent) _HOOKS_IN_GIT_DIR="no" case "$_HOOKS_DIR" in - "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; + "$_GIT_DIR"/*|"$_GIT_COMMON"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; esac _PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index 068ac4fe5..bd50a20c8 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -393,9 +393,15 @@ _HOOK_INSTALLED="no" # committed hook and write a machine-local wrapper into the working tree. _HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") _GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +# Linked worktrees: --absolute-git-dir is .git/worktrees/ but hooks +# resolve to the COMMON .git/hooks, so match against the common dir too or +# every Conductor worktree false-negatives as a "custom hooks path". The +# /nonexistent fallback keeps the case pattern from collapsing to "/*" +# (match-everything) when resolution fails. +_GIT_COMMON=$(cd "$(git rev-parse --git-common-dir 2>/dev/null || echo /nonexistent)" 2>/dev/null && pwd || echo /nonexistent) _HOOKS_IN_GIT_DIR="no" case "$_HOOKS_DIR" in - "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; + "$_GIT_DIR"/*|"$_GIT_COMMON"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; esac _PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index c86722534..4730797b0 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -1272,9 +1272,15 @@ _HOOK_INSTALLED="no" # committed hook and write a machine-local wrapper into the working tree. _HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") _GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +# Linked worktrees: --absolute-git-dir is .git/worktrees/ but hooks +# resolve to the COMMON .git/hooks, so match against the common dir too or +# every Conductor worktree false-negatives as a "custom hooks path". The +# /nonexistent fallback keeps the case pattern from collapsing to "/*" +# (match-everything) when resolution fails. +_GIT_COMMON=$(cd "$(git rev-parse --git-common-dir 2>/dev/null || echo /nonexistent)" 2>/dev/null && pwd || echo /nonexistent) _HOOKS_IN_GIT_DIR="no" case "$_HOOKS_DIR" in - "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; + "$_GIT_DIR"/*|"$_GIT_COMMON"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; esac _PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index 7394a3ab3..4ac024a20 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -2455,9 +2455,15 @@ _HOOK_INSTALLED="no" # committed hook and write a machine-local wrapper into the working tree. _HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") _GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +# Linked worktrees: --absolute-git-dir is .git/worktrees/ but hooks +# resolve to the COMMON .git/hooks, so match against the common dir too or +# every Conductor worktree false-negatives as a "custom hooks path". The +# /nonexistent fallback keeps the case pattern from collapsing to "/*" +# (match-everything) when resolution fails. +_GIT_COMMON=$(cd "$(git rev-parse --git-common-dir 2>/dev/null || echo /nonexistent)" 2>/dev/null && pwd || echo /nonexistent) _HOOKS_IN_GIT_DIR="no" case "$_HOOKS_DIR" in - "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; + "$_GIT_DIR"/*|"$_GIT_COMMON"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; esac _PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH" diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index 23ecd4f98..a5539b750 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -2861,9 +2861,15 @@ _HOOK_INSTALLED="no" # committed hook and write a machine-local wrapper into the working tree. _HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo "") _GIT_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || echo "") +# Linked worktrees: --absolute-git-dir is .git/worktrees/ but hooks +# resolve to the COMMON .git/hooks, so match against the common dir too or +# every Conductor worktree false-negatives as a "custom hooks path". The +# /nonexistent fallback keeps the case pattern from collapsing to "/*" +# (match-everything) when resolution fails. +_GIT_COMMON=$(cd "$(git rev-parse --git-common-dir 2>/dev/null || echo /nonexistent)" 2>/dev/null && pwd || echo /nonexistent) _HOOKS_IN_GIT_DIR="no" case "$_HOOKS_DIR" in - "$_GIT_DIR"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; + "$_GIT_DIR"/*|"$_GIT_COMMON"/*|hooks|.git/hooks) _HOOKS_IN_GIT_DIR="yes" ;; esac _PREPUSH_PROMPTED=$([ -f "${GSTACK_HOME:-$HOME/.gstack}/.redact-prepush-prompted" ] && echo "yes" || echo "no") echo "REDACT_PREPUSH: $_REDACT_PREPUSH"