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/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new 0b84140  feat(setup-isolated): per-project vs whole-user scope choice 
(#199)
0b84140 is described below

commit 0b84140827946c0b504bf2ea4945c0b403811184
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sun May 17 21:39:01 2026 +0200

    feat(setup-isolated): per-project vs whole-user scope choice (#199)
    
    setup-isolated-setup-install now lets the operator choose between
    per-project scope (the existing behavior — configure the current
    adopter repo only) and whole-user scope (configure every
    Claude-Code-aware git repo on the host, existing and future, via
    a global core.hooksPath hook).
    
    What landed:
    
    - tools/agent-isolation/git-global-post-checkout.sh: new universal
      post-checkout hook installed at ~/.claude/git-hooks/post-checkout
      under whole-user scope. Calls setup-steward verify --auto-fix-symlinks
      for steward-adopted repos and sandbox-add-project-root.sh for any
      project with a .claude/ directory. Best-effort, idempotent, never
      fails the git operation.
    
    - setup-isolated-setup-install Step P restructured:
      - P.0 asks per-project vs whole-user up front (single-select).
      - P.0a (whole-user only) surfaces a loud disclosure of the
        core.hooksPath shadowing trade-off and requires explicit
        acknowledgement.
      - P.1 installs the script (both scopes).
      - P.2 runs the helper for the current project (per-project).
      - P.2-whole-user walks the operator's existing checkouts under
        prompted root dirs (defaults: ~/code, ~/projects, ~/dev,
        ~/work; depth-limited find with noise exclusions) and writes
        settings.local.json in each. Settings-only — no per-repo hook
        installs.
      - P.3-whole-user installs the global post-checkout +
        git config --global core.hooksPath ~/.claude/git-hooks/.
    
    - setup-isolated-setup-verify Check 8: scope-detection sub-check
      reads git config --global core.hooksPath; verifies global
      post-checkout presence + drift if whole-user scope is detected.
      Surfaces an informational note every run when whole-user scope
      is in effect.
    
    - setup-isolated-setup-update: drift-check coverage extended to
      include ~/.claude/git-hooks/post-checkout when whole-user scope
      is detected.
    
    - docs/setup/secure-agent-setup.md: new subsection
      "Per-project vs whole-user scope" with the comparison table,
      the core.hooksPath shadowing trade-off, when-to-pick-which
      guidance, and the reversal command.
    
    - tools/agent-isolation/README.md: row for the new helper.
    
    Generated-by: Claude Code (Opus 4.7)
---
 .../skills/setup-isolated-setup-install/SKILL.md   | 230 +++++++++++++++++----
 .../skills/setup-isolated-setup-update/SKILL.md    |  16 +-
 .../skills/setup-isolated-setup-verify/SKILL.md    |  32 +++
 docs/setup/secure-agent-setup.md                   |  62 ++++++
 tools/agent-isolation/README.md                    |   1 +
 tools/agent-isolation/git-global-post-checkout.sh  |  93 +++++++++
 6 files changed, 393 insertions(+), 41 deletions(-)

diff --git a/.claude/skills/setup-isolated-setup-install/SKILL.md 
b/.claude/skills/setup-isolated-setup-install/SKILL.md
index 2d3a836..09c5148 100644
--- a/.claude/skills/setup-isolated-setup-install/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-install/SKILL.md
@@ -160,44 +160,198 @@ user-scope by the harness. Worktrees handle themselves: 
each
 worktree has its own `<worktree>/.claude/settings.local.json`,
 and each gets its own root added.
 
-Two install sub-steps cover this:
-
-1. **Install the helper script.** Copy
-   `tools/agent-isolation/sandbox-add-project-root.sh` into
-   `~/.claude/scripts/sandbox-add-project-root.sh` (or symlink it
-   from `~/.claude-config/scripts/` if the operator uses the
-   private sync repo), mode `0755`. The script file lives
-   user-scope so a single install covers every adopter project on
-   the host; what it **writes** is project-local. The same install
-   mechanism the `sandbox-bypass-warn.sh` and `sandbox-status-line.sh`
-   helpers use (see the *Sandbox-bypass visibility hook* and
-   *Sandbox-state status line* sections of the doc).
-2. **Run the helper once with `--all-worktrees`** in the adopter
-   repo's main checkout. The helper enumerates
-   `git worktree list --porcelain` and, for each worktree, writes
-   that worktree's absolute path into that worktree's own
-   `<worktree>/.claude/settings.local.json` (creating the file if
-   it does not yet exist). Idempotent, atomic, tolerant of missing
-   prereqs (see the script's header comment for the full
-   failure-mode list). On success, surface the diff so the operator
-   sees which entries landed; on no-op (paths already present),
-   surface a one-line "already covered" confirmation.
-
-   **Sandbox-bypass requirement when invoked from inside an agent
-   session.** `.claude/settings.local.json` is in Claude Code's
-   built-in sandbox `denyWithinAllow` set (verified empirically —
-   see
-   [`docs/setup/secure-agent-setup.md` → *Security 
rationale*](../../../docs/setup/secure-agent-setup.md#security-rationale--why-project-local-is-safe-to-write-to)),
-   so the helper's Bash write is blocked when invoked through the
-   agent's `Bash` tool. If this skill is being walked from inside
-   a sandboxed session, invoke the helper with
-   `dangerouslyDisableSandbox: true` and the reason
-   *"writing project-local sandbox-allowlist entries (issue #197 fix)"*.
-   The bypass triggers `sandbox-bypass-warn.sh`'s loud-red banner
-   so the operator sees and approves the single write. When the
-   operator runs `setup-isolated-setup-install` directly from a
-   terminal (the typical first-time-install path), no bypass is
-   needed — the script runs outside the agent sandbox.
+#### Step P.0 — Ask the user: per-project or whole-user scope?
+
+Before installing anything, ask the operator which scope they
+want — see
+[`docs/setup/secure-agent-setup.md` → *Per-project vs whole-user 
scope*](../../../docs/setup/secure-agent-setup.md#per-project-vs-whole-user-scope)
+for the full rationale + trade-offs:
+
+- **Per-project (default).** The skill runs the helper for the
+  current project only. Future projects on this host need the
+  skill re-run in each, OR the operator can pick whole-user later.
+  No global git config changes.
+- **Whole-user.** The skill walks the operator's existing local
+  git checkouts and populates their `settings.local.json` files,
+  then sets `git config --global core.hooksPath` so every future
+  `git checkout` / `git clone` / `git worktree add` on the host
+  picks up the framework's universal post-checkout hook.
+
+**Prefer structured Q&A.** When the agent harness offers a
+structured-question tool (e.g. Claude Code's `AskUserQuestion`),
+use a single-select prompt with `Per-project` as the default and
+`Whole-user (with caveats)` as the alternative. Free-form chat is
+the fallback.
+
+If the user picks **per-project**, skip to *Step P.1 — Install
+the helper script* and *Step P.2 — Run the helper for this project*,
+then move on to the next step in the canonical install list.
+
+If the user picks **whole-user**, follow Step P.0a's loud
+disclosure first, then Step P.1, then Steps P.2-whole-user (walk
+existing checkouts) and P.3-whole-user (install the global hook
++ set `core.hooksPath`).
+
+##### Step P.0a — Loud disclosure before setting whole-user scope
+
+If the operator picked whole-user, surface this disclosure
+**before** any global config write. The operator must
+acknowledge it explicitly; no silent proceed:
+
+> **!!! WHOLE-USER SCOPE — `core.hooksPath` GLOBAL OVERRIDE !!!**
+>
+> Setting `git config --global core.hooksPath` makes git look up
+> hooks in **one shared directory** for every repo on this host.
+> Every `.git/hooks/*` in every existing repo on this machine
+> becomes **inert** — git will no longer fire your per-repo
+> `pre-commit`, `commit-msg`, `pre-push`, or any other hook
+> unless you migrate it into the shared dir.
+>
+> The framework installs **only** the `post-checkout` hook in the
+> shared dir. If you rely on per-repo hooks today (formatters,
+> linters, CI integration), you need to:
+>
+> - Either migrate them into `~/.claude/git-hooks/` so they fire
+>   alongside the framework's `post-checkout`, **or**
+> - Pick **per-project** scope instead and re-run this skill in
+>   each project you adopt.
+>
+> Whole-user scope is reversible: `git config --global --unset core.hooksPath`
+> restores per-repo hook lookup.
+
+Confirm the operator wants to proceed with whole-user scope after
+reading the disclosure. If they hesitate or pick per-project,
+fall back to the per-project path.
+
+#### Step P.1 — Install the helper script
+
+(Both scopes.)
+
+Copy `tools/agent-isolation/sandbox-add-project-root.sh` into
+`~/.claude/scripts/sandbox-add-project-root.sh` (or symlink it
+from `~/.claude-config/scripts/` if the operator uses the
+private sync repo), mode `0755`. The script file lives
+user-scope so a single install covers every adopter project on
+the host; what it **writes** is project-local. The same install
+mechanism the `sandbox-bypass-warn.sh` and `sandbox-status-line.sh`
+helpers use (see the *Sandbox-bypass visibility hook* and
+*Sandbox-state status line* sections of the doc).
+#### Step P.2 — Run the helper for this project (per-project scope)
+
+Skip if the operator picked whole-user scope (Step P.2-whole-user
+below covers the equivalent).
+
+Run the helper once with `--all-worktrees` in the adopter
+repo's main checkout. The helper enumerates
+`git worktree list --porcelain` and, for each worktree, writes
+that worktree's absolute path into that worktree's own
+`<worktree>/.claude/settings.local.json` (creating the file if
+it does not yet exist). Idempotent, atomic, tolerant of missing
+prereqs (see the script's header comment for the full
+failure-mode list). On success, surface the diff so the operator
+sees which entries landed; on no-op (paths already present),
+surface a one-line "already covered" confirmation.
+
+**Sandbox-bypass requirement when invoked from inside an agent
+session.** `.claude/settings.local.json` is in Claude Code's
+built-in sandbox `denyWithinAllow` set (verified empirically —
+see
+[`docs/setup/secure-agent-setup.md` → *Security 
rationale*](../../../docs/setup/secure-agent-setup.md#security-rationale--why-project-local-is-safe-to-write-to)),
+so the helper's Bash write is blocked when invoked through the
+agent's `Bash` tool. If this skill is being walked from inside
+a sandboxed session, invoke the helper with
+`dangerouslyDisableSandbox: true` and the reason
+*"writing project-local sandbox-allowlist entries (issue #197 fix)"*.
+The bypass triggers `sandbox-bypass-warn.sh`'s loud-red banner
+so the operator sees and approves the single write. When the
+operator runs `setup-isolated-setup-install` directly from a
+terminal (the typical first-time-install path), no bypass is
+needed — the script runs outside the agent sandbox.
+
+#### Step P.2-whole-user — Walk existing checkouts (whole-user scope)
+
+Skip if the operator picked per-project scope.
+
+Walk the operator's existing git checkouts and populate each
+one's `.claude/settings.local.json`. This pass is **settings-only**
+by default — it does NOT install per-repo `post-checkout` hooks
+(the global hook installed in Step P.3-whole-user covers that
+for both existing and future repos via `core.hooksPath`).
+
+1. **Prompt the operator for root directories to scan.**
+   Default suggestions: `~/code/`, `~/projects/`, `~/dev/`,
+   `~/work/`. Show the operator the list, let them edit it.
+   Empty list → skip the walk; the operator can re-run this
+   skill later when they want existing repos covered.
+
+2. **Walk each root dir.** Use a depth-limited `find` with
+   reasonable exclusions:
+
+   ```bash
+   find "<root>" -maxdepth 5 -type d -name .git \
+       -not -path '*/node_modules/*' \
+       -not -path '*/.venv/*' \
+       -not -path '*/__pycache__/*' \
+       -not -path '*/build/*' \
+       -not -path '*/dist/*' \
+       -not -path '*/.cache/*' \
+       -prune
+   ```
+
+   For each `.git/` found, the parent dir is a working tree.
+   De-duplicate by canonical path.
+
+3. **For each working tree found, run the helper with
+   `--all-worktrees`.** Same invocation as the per-project
+   variant — the helper itself handles `git worktree list` so
+   linked worktrees of the same repo get processed. The helper's
+   built-in `git check-ignore` guard skips repos whose
+   `.claude/settings.local.json` is not gitignored (defense in
+   depth — the operator should fix the `.gitignore` first).
+
+4. **Tabulate the result** for the operator: how many checkouts
+   were scanned, how many had `.claude/` (so the helper wrote),
+   how many were skipped (no `.claude/` directory, or
+   `.claude/settings.local.json` not gitignored).
+
+5. **Do not install per-repo `post-checkout` hooks** during this
+   pass. The next sub-step covers future and existing repos
+   uniformly via the global hook.
+
+#### Step P.3-whole-user — Install the global post-checkout hook (whole-user 
scope)
+
+Skip if the operator picked per-project scope.
+
+1. **Install the universal `post-checkout` hook.** Copy
+   `tools/agent-isolation/git-global-post-checkout.sh` into
+   `~/.claude/git-hooks/post-checkout` (or symlink it from
+   `~/.claude-config/git-hooks/post-checkout` if the operator
+   uses the private sync repo), mode `0755`. The hook content is
+   in
+   
[`tools/agent-isolation/git-global-post-checkout.sh`](../../../tools/agent-isolation/git-global-post-checkout.sh)
 —
+   it calls the sandbox-allowlist helper and (for steward-adopted
+   repos) `setup-steward verify --auto-fix-symlinks`.
+
+2. **Set `core.hooksPath` globally** so every git operation across
+   every repo on the host uses the shared hook dir:
+
+   ```bash
+   git config --global core.hooksPath "$HOME/.claude/git-hooks"
+   ```
+
+   Surface the resulting `git config --global --get core.hooksPath`
+   value to the operator to confirm the write.
+
+3. **Reiterate the implication** from Step P.0a's disclosure:
+   per-repo `.git/hooks/*` are now inert across the entire host.
+   The operator must migrate any per-repo hooks they want to keep.
+   `git config --global --unset core.hooksPath` is the reversal.
+
+After this step, future `git clone`, `git worktree add`, and
+`git checkout` operations anywhere on the host invoke the
+framework's universal post-checkout, which keeps each
+Claude-Code-aware project's `.claude/settings.local.json` in
+sync without any further operator action.
 
 The `.` entry stays in the committed project-scope `allowRead`
 regardless — the explicit absolute path in
diff --git a/.claude/skills/setup-isolated-setup-update/SKILL.md 
b/.claude/skills/setup-isolated-setup-update/SKILL.md
index 8736c26..14d55b0 100644
--- a/.claude/skills/setup-isolated-setup-update/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-update/SKILL.md
@@ -126,9 +126,19 @@ Walk each:
    `~/.claude/scripts/sandbox-status-line.sh` or whatever the
    user's actual statusLine command resolves to,
    `~/.claude/agent-isolation/claude-iso.sh` for the global
-   wrapper install), `diff` the user copy against the framework's
-   source-of-truth in `tools/agent-isolation/`. Report any drift
-   as a unified diff; do not re-`cp`.
+   wrapper install,
+   `~/.claude/scripts/sandbox-add-project-root.sh` for the
+   issue-#197 project-root helper, **and** —
+   *only when whole-user scope is in effect, detected via
+   `git config --global --get core.hooksPath` resolving to
+   `~/.claude/git-hooks`* —
+   `~/.claude/git-hooks/post-checkout` for the universal
+   post-checkout hook), `diff` the user copy against the
+   framework's source-of-truth in `tools/agent-isolation/`.
+   Report any drift as a unified diff; do not re-`cp`. The
+   re-install path for each is
+   [`setup-isolated-setup-install`](../setup-isolated-setup-install/SKILL.md)
+   re-run on the affected Step P sub-step.
 4. **Settings.json shape drift.** Diff the user's project
    `.claude/settings.json` against the framework's dogfooded
    one — the framework occasionally adds new `denyRead` paths
diff --git a/.claude/skills/setup-isolated-setup-verify/SKILL.md 
b/.claude/skills/setup-isolated-setup-verify/SKILL.md
index 1404d32..d4382cd 100644
--- a/.claude/skills/setup-isolated-setup-verify/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-verify/SKILL.md
@@ -185,6 +185,38 @@ Walk each in order:
    [`docs/setup/secure-agent-setup.md` → *Project-root coverage in the sandbox 
allowlists*](../../../docs/setup/secure-agent-setup.md#project-root-coverage-in-the-sandbox-allowlists)
    for why.
 
+   **Scope detection (per-project vs whole-user).** The install
+   skill offers two scopes. Detect which one is in effect:
+
+   ```bash
+   git config --global --get core.hooksPath
+   ```
+
+   If the output equals `$HOME/.claude/git-hooks` (or its tilde-
+   resolved form), the operator is in **whole-user** scope:
+
+   - ✓ if `~/.claude/git-hooks/post-checkout` exists, is
+     executable, and matches the framework's
+     `tools/agent-isolation/git-global-post-checkout.sh` content.
+   - ⚠ if the hook is missing or non-executable — the `core.hooksPath`
+     pointer is set but the hook content is gone. Remediation:
+     re-run `/setup-isolated-setup-install` Step P.3-whole-user,
+     or `/setup-isolated-setup-update` to refresh the script copy.
+   - ⚠ if the hook content drifted from the framework's source-of-
+     truth — surface the diff, propose `/setup-isolated-setup-update`.
+   - **Loud reminder** (every run, not a ✗): when in whole-user
+     scope, surface a one-line note that per-repo `.git/hooks/*`
+     are inert across the host (per [`docs/setup/secure-agent-setup.md` → 
*Per-project vs whole-user 
scope*](../../../docs/setup/secure-agent-setup.md#per-project-vs-whole-user-scope)).
+     This is informational, not a failure — the operator chose it
+     deliberately during install. Surface so a future self
+     debugging "why didn't my pre-commit fire" recognises the
+     cause.
+
+   If `core.hooksPath` is unset (or points elsewhere), the
+   operator is in **per-project** scope (the default). No further
+   sub-check needed — the per-project mode is fully covered by
+   the static + live-probe checks above.
+
 ## After the report
 
 If every check is ✓, say so explicitly and stop — no further
diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md
index 4d5c8a2..ef60f96 100644
--- a/docs/setup/secure-agent-setup.md
+++ b/docs/setup/secure-agent-setup.md
@@ -17,6 +17,7 @@
     - [Security rationale — why project-local is safe to write 
to](#security-rationale--why-project-local-is-safe-to-write-to)
     - [`sandbox-add-project-root.sh`](#sandbox-add-project-rootsh)
     - [When the helper runs](#when-the-helper-runs)
+    - [Per-project vs whole-user scope](#per-project-vs-whole-user-scope)
   - [The clean-env wrapper](#the-clean-env-wrapper)
   - [Sandbox-bypass visibility hook](#sandbox-bypass-visibility-hook)
     - [Why install it user-scope, not 
project-scope](#why-install-it-user-scope-not-project-scope)
@@ -621,6 +622,67 @@ The verification surface:
   Check 8b — static cross-check that the current worktree's
   abs path is in its own `.claude/settings.local.json`.
 
+### Per-project vs whole-user scope
+
+[`setup-isolated-setup-install`](../../.claude/skills/setup-isolated-setup-install/SKILL.md)
+offers two scopes for the project-root sandbox-allowlist setup.
+The operator picks one during install; both are reversible.
+
+| Scope | What it covers | Mechanism | Reversal |
+|---|---|---|---|
+| **Per-project** (default) | The single adopter repo the operator is sitting 
in when running the install skill. Each subsequent adopter project needs the 
install skill re-run there. | The helper runs once with `--all-worktrees` 
against the current repo; nothing global is touched. The per-repo 
`post-checkout` hook (installed by `/setup-steward adopt` in steward-adopted 
repos) chains into the helper on future `git checkout` operations within that 
repo. | None needed — per-project scope is [...]
+| **Whole-user** | Every git repo on the operator's host, existing and future. 
Includes non-steward Claude-Code-aware projects (any project with a `.claude/` 
directory). | Walks the operator's existing checkouts under prompted root dirs 
and writes each one's `settings.local.json`; sets `git config --global 
core.hooksPath ~/.claude/git-hooks/` and installs the universal 
[`git-global-post-checkout.sh`](../../tools/agent-isolation/git-global-post-checkout.sh)
 there. | `git config --global - [...]
+
+#### Important trade-off — `core.hooksPath` shadows per-repo hooks
+
+When `core.hooksPath` is set globally, git looks up hooks **only**
+in that directory for every repo on the host. Every per-repo
+`<repo>/.git/hooks/*` becomes inert across the host. If the
+operator has hooks they care about (pre-commit formatters,
+commit-msg linters, pre-push gates, project-specific
+post-checkout actions), those will no longer fire after whole-user
+scope is set, unless the operator migrates them into
+`~/.claude/git-hooks/`.
+
+The framework installs **only** the `post-checkout` hook in the
+shared dir. Pre-commit / commit-msg / pre-push / other hook types
+need their own files in the shared dir if the operator wants
+them to fire. This is a deliberate trade-off: a single mechanism
+for whole-user coverage at the cost of needing to migrate
+per-repo hooks.
+
+The install skill surfaces this trade-off loudly before setting
+`core.hooksPath` and requires explicit operator acknowledgement.
+See
+[`setup-isolated-setup-install` Step 
P.0a](../../.claude/skills/setup-isolated-setup-install/SKILL.md#step-p0a--loud-disclosure-before-setting-whole-user-scope).
+
+#### When to pick which scope
+
+- **Pick per-project** when:
+  - You adopt one or two projects on this host and prefer not to
+    touch global git config.
+  - You have per-repo hooks (pre-commit, commit-msg, etc.) you
+    rely on and do not want shadowed.
+  - You are evaluating apache-steward and have not yet decided
+    whether to commit to the framework.
+
+- **Pick whole-user** when:
+  - You adopt many Claude-Code-aware projects and do not want to
+    re-run the install skill in each.
+  - You add worktrees frequently and want each one's
+    `settings.local.json` auto-populated without per-worktree
+    action.
+  - You do not rely on per-repo hooks (or are prepared to migrate
+    them into the shared dir).
+  - You sync `~/.claude/` across machines via the private dotfile
+    repo (the global config + hook propagates with the sync).
+
+Switching scopes later is non-destructive: the install skill is
+idempotent. Re-running it with a different scope is the supported
+upgrade path. The walking pass under whole-user scope is also a
+one-time bulk operation — once existing checkouts are populated,
+the global `post-checkout` keeps everything aligned going forward.
+
 ## The clean-env wrapper
 
 Layer 0 — strip credential-shaped env vars from the parent shell
diff --git a/tools/agent-isolation/README.md b/tools/agent-isolation/README.md
index 2a7a70a..fb5c015 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -32,6 +32,7 @@ versions.
 | [`sandbox-status-line.sh`](sandbox-status-line.sh) | Claude Code 
`statusLine` helper. Renders `<model> [sandbox]` (green) or `<model> [NO 
SANDBOX]` (bold red) based on `sandbox.enabled` in the active settings — 
project `settings.local.json` first, then project `settings.json`, then 
user-scope, mirroring Claude Code's own precedence. Reflects in-session 
`/sandbox` toggles (which persist to project `settings.local.json`). 
Recommended user-scope. |
 | [`sandbox-status-line-rich.sh`](sandbox-status-line-rich.sh) | Opt-in richer 
alternative to `sandbox-status-line.sh`. Same sandbox-state detection, plus 
folder name (hash-coloured), git branch + dirty + ahead/behind, per-branch PR 
title (cached, gated by `gh`), and a yellow `[sandbox-auto]` tag for the 
`autoAllowBashIfSandboxed` setting. Wire one *or* the other into 
`statusLine.command`. |
 | [`sandbox-add-project-root.sh`](sandbox-add-project-root.sh) | Adds the 
current adopter repo's project root (and, with `--all-worktrees`, every linked 
git worktree's working dir) as an explicit absolute path to 
`sandbox.filesystem.allowRead` and `allowWrite` in the project-local, 
gitignored `<repo>/.claude/settings.local.json` — one entry per worktree, each 
in that worktree's own settings file. Defensive against [issue 
#197](https://github.com/apache/airflow-steward/issues/197) — `allo [...]
+| [`git-global-post-checkout.sh`](git-global-post-checkout.sh) | Universal 
`post-checkout` git hook installed at `~/.claude/git-hooks/post-checkout` when 
the operator picks **whole-user** scope in `setup-isolated-setup-install`. 
Activated by `git config --global core.hooksPath ~/.claude/git-hooks/` so every 
`git checkout` / `git clone` / `git worktree add` across the host invokes it. 
Two responsibilities (both best-effort + idempotent + `\|\| true`): (1) 
`setup-steward verify --auto-fix- [...]
 
 ## Usage at a glance
 
diff --git a/tools/agent-isolation/git-global-post-checkout.sh 
b/tools/agent-isolation/git-global-post-checkout.sh
new file mode 100755
index 0000000..ba2990f
--- /dev/null
+++ b/tools/agent-isolation/git-global-post-checkout.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# git-global-post-checkout.sh — the universal post-checkout hook
+# installed under ~/.claude/git-hooks/post-checkout when the
+# operator picks **whole-user** scope in
+# `setup-isolated-setup-install`.
+#
+# Activated by:
+#   git config --global core.hooksPath ~/.claude/git-hooks/
+#
+# After that, every `git checkout`, `git clone` (which runs an
+# implicit checkout of the default branch), and `git worktree add`
+# across the operator's host invokes this script — for any repo,
+# not just apache-steward adopters.
+#
+# Two responsibilities, both best-effort + idempotent + `|| true`
+# so the hook never breaks the surrounding git operation:
+#
+#   1. **apache-steward symlink reconciliation.** If the working
+#      tree carries `.apache-steward.lock`, this is a steward-
+#      adopted repo and its gitignored framework-skill symlinks
+#      may need re-creating after a checkout (the symlinks point
+#      into `.apache-steward/`, which is itself gitignored). The
+#      hook calls `/setup-steward verify --auto-fix-symlinks` if
+#      that command is on PATH; if not, it falls through silently
+#      (the operator may not be in a Claude Code session, or may
+#      not have the framework installed beyond this hook).
+#
+#   2. **Sandbox-allowlist for the current worktree.** If the
+#      working tree has a `.claude/` directory (i.e. it is
+#      Claude-Code-aware) and the framework's
+#      `sandbox-add-project-root.sh` helper is installed at
+#      `~/.claude/scripts/`, the hook calls the helper to populate
+#      `<worktree>/.claude/settings.local.json` with the
+#      worktree's absolute path. Defensive against the harness
+#      behaviour documented at
+#      https://github.com/apache/airflow-steward/issues/197 .
+#
+# IMPORTANT — `core.hooksPath` shadowing. When `core.hooksPath` is
+# set globally, git looks ONLY in that directory for hooks. Every
+# per-repo `<repo>/.git/hooks/*` becomes inert across every repo
+# on the host. If the operator has hooks they care about in
+# specific repos (per-commit linters, pre-push checks, etc.) those
+# need to be migrated into the global hooks dir or invoked from
+# within these global hooks. The install skill warns the operator
+# about this trade-off loudly before setting `core.hooksPath`.
+#
+# This hook only handles `post-checkout`. Other hook types
+# (pre-commit, commit-msg, pre-push, ...) need their own files in
+# the global hooks dir if the operator wants them to fire after
+# setting `core.hooksPath`. The framework does not ship them; it
+# is up to the operator to migrate them.
+
+set -u
+
+# Resolve the current working tree's root. The hook fires inside a
+# git checkout — `git rev-parse --show-toplevel` should always
+# succeed; the `2>/dev/null` is defensive against odd worktree
+# states.
+worktree=$(git rev-parse --show-toplevel 2>/dev/null || true)
+if [ -z "$worktree" ]; then
+  exit 0
+fi
+
+# 1. apache-steward symlink reconciliation
+if [ -f "$worktree/.apache-steward.lock" ] \
+    && command -v /setup-steward >/dev/null 2>&1; then
+  /setup-steward verify --auto-fix-symlinks 2>/dev/null || true
+fi
+
+# 2. Sandbox-allowlist helper
+if [ -x "$HOME/.claude/scripts/sandbox-add-project-root.sh" ] \
+    && [ -d "$worktree/.claude" ]; then
+  "$HOME/.claude/scripts/sandbox-add-project-root.sh" 2>/dev/null || true
+fi
+
+exit 0

Reply via email to