From 32888d6247cc2cda6b2253e6598633ebe9331426 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 21:20:47 +0000 Subject: [PATCH 1/3] Initial plan From e44f4f80ece2ae410e41f1097cc3406d145862f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 21:31:05 +0000 Subject: [PATCH 2/3] fix: provide scope-lock-apply helper to unblock scope-lock write step The pre-tool-scope-guard hook blocks Write/Edit/MultiEdit tool calls to *.scope-lock paths unconditionally. The scope-lock skill had no in-session way to write the lock file: using the Write tool was blocked, and setting SUPERPOWERS_SCOPE_LOCK_WRITE=1 via Bash was also blocked by the self-bypass prevention logic. Fix: add hooks/scope-lock-apply, a dedicated helper the scope-lock skill invokes via the Bash tool. Shell redirection is not gated by the Write tool guard, so the hook never fires. The script extracts the Scope Manifest section using the same awk pattern as tests/plan-scope-check.sh, computes sha256 portably (sha256sum or shasum -a 256), and writes .scope-lock. Update SKILL.md (scope-lock and alignment-check) to replace the naive "compute sha256 and write the file" instruction with the explicit helper invocation. Update the pre-tool-scope-guard block message to point agents at the helper instead of the now-misleading SUPERPOWERS_SCOPE_LOCK_WRITE env var note. Agent-Logs-Url: https://github.com/GoCodeAlone/claude-superpowers/sessions/5f1aafea-9d74-4c24-be43-32e5f16de6b7 Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- hooks/pre-tool-scope-guard | 4 +- hooks/scope-lock-apply | 74 +++++++++++++++++++++++++++++++++ skills/alignment-check/SKILL.md | 2 +- skills/scope-lock/SKILL.md | 6 ++- 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100755 hooks/scope-lock-apply diff --git a/hooks/pre-tool-scope-guard b/hooks/pre-tool-scope-guard index 22d9515..3b7c67f 100755 --- a/hooks/pre-tool-scope-guard +++ b/hooks/pre-tool-scope-guard @@ -10,7 +10,7 @@ # — git push / gh pr create when locked plan hash mismatches # — git push / git commit to main or master (unless SUPERPOWERS_ALLOW_DEFAULT_BRANCH=1) # Write / Edit / MultiEdit tool -# — *.scope-lock files (unless SUPERPOWERS_SCOPE_LOCK_WRITE=1) +# — *.scope-lock files (use hooks/scope-lock-apply via Bash instead) # — docs/plans/*.md when plan is Locked (unless SUPERPOWERS_PLAN_LOCK_WRITE=1) # # Global opt-out: set SUPERPOWERS_HOOKS_DISABLE=1 in the *operator's* terminal @@ -144,7 +144,7 @@ case "$tool_name" in # ── 5. scope-lock files (always blocked unless sentinel env var) ───── if printf '%s' "$fpath" | grep -qE '\.scope-lock$'; then if [ "${SUPERPOWERS_SCOPE_LOCK_WRITE:-}" != "1" ]; then - block "Writing to '$(basename "$fpath")' is blocked — .scope-lock files are written exclusively by the scope-lock skill during alignment-check PASS. Direct edits break the manifest integrity guarantee and allow silent scope tampering. To update the lock legitimately: go through the unlock path (recording-decisions → update manifest → re-run alignment-check), which will regenerate the lock file." + block "Writing to '$(basename "$fpath")' is blocked — .scope-lock files must be written by the scope-lock skill's helper, not via the Write tool. Run: bash \"\${CLAUDE_PLUGIN_ROOT}/hooks/scope-lock-apply\" . The helper extracts the Scope Manifest section, computes its sha256, and writes the lock file via shell redirection (which is not blocked). Direct Write/Edit calls break the manifest integrity guarantee and allow silent scope tampering. To update the lock legitimately after a scope reduction: go through the unlock path (recording-decisions → update manifest → re-run alignment-check → re-run scope-lock-apply)." fi fi diff --git a/hooks/scope-lock-apply b/hooks/scope-lock-apply new file mode 100755 index 0000000..5dc858a --- /dev/null +++ b/hooks/scope-lock-apply @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# hooks/scope-lock-apply +# Compute and write the .scope-lock file for a plan. +# +# Usage: scope-lock-apply +# +# Extracts the "## Scope Manifest" section from , computes its +# sha256, and writes the digest to .scope-lock via shell +# redirection. +# +# This helper exists because the Write/Edit/MultiEdit tools are blocked for +# *.scope-lock paths by hooks/pre-tool-scope-guard (to prevent silent scope +# tampering by implementation skills or subagents). The scope-lock skill is +# the only legitimate writer of these files, and calling this helper via the +# Bash tool is the sanctioned path — it uses shell redirection rather than +# the Write tool, so the guard never fires. +# +# The hash format is a single bare hex sha256 digest, which is exactly what +# tests/plan-scope-check.sh --verify-lock expects: +# expected="$(awk 'NF && !/^#/ {print; exit}' "$lock")" + +set -euo pipefail + +plan="${1:-}" + +if [ -z "$plan" ]; then + printf 'Usage: scope-lock-apply \n' >&2 + exit 3 +fi + +if [ ! -f "$plan" ]; then + printf 'Error: plan file not found: %s\n' "$plan" >&2 + exit 1 +fi + +# Verify the plan has a Scope Manifest section before attempting to hash it. +if ! grep -qE '^## Scope Manifest[[:space:]]*$' "$plan"; then + printf 'Error: no "## Scope Manifest" section found in %s\n' "$plan" >&2 + printf 'The manifest section is required before locking. See skills/scope-lock/SKILL.md.\n' >&2 + exit 1 +fi + +# Compute sha256 of the manifest section. +# The awk pattern below is intentionally identical to extract_manifest() in +# tests/plan-scope-check.sh — the byte stream must match exactly so that +# tests/plan-scope-check.sh --verify-lock produces the same hash. +# Extract to a temp file to avoid duplicating the awk script across sha256 +# backend branches. +tmpfile=$(mktemp) +trap 'rm -f "$tmpfile"' EXIT + +awk ' + /^## Scope Manifest[[:space:]]*$/ { in_section = 1; print; next } + in_section && /^## / { in_section = 0 } + in_section { print } +' "$plan" > "$tmpfile" + +if command -v sha256sum >/dev/null 2>&1; then + hash=$(sha256sum < "$tmpfile" | awk '{print $1}') +elif command -v shasum >/dev/null 2>&1; then + hash=$(shasum -a 256 < "$tmpfile" | awk '{print $1}') +else + printf 'Error: sha256sum or shasum is required but neither was found\n' >&2 + exit 1 +fi + +if [ -z "$hash" ]; then + printf 'Error: failed to compute sha256 for %s\n' "$plan" >&2 + exit 1 +fi + +lock_file="${plan}.scope-lock" +printf '%s\n' "$hash" > "$lock_file" +printf 'scope-lock-apply: %s written (sha256=%s…)\n' "$lock_file" "${hash:0:12}" diff --git a/skills/alignment-check/SKILL.md b/skills/alignment-check/SKILL.md index 082407e..879b131 100644 --- a/skills/alignment-check/SKILL.md +++ b/skills/alignment-check/SKILL.md @@ -130,7 +130,7 @@ Re-run alignment check after revision. **Max 2 revision cycles** before escalati After alignment passes, **lock the plan's scope** so subsequent execution cannot silently rescope. Invoke `superpowers:scope-lock` with the plan path. The scope-lock skill: 1. Stamps the plan's `**Status:**` line with `Locked `. -2. Computes the manifest's sha256 and writes `.scope-lock`. +2. Writes the lock file by running `bash "${CLAUDE_PLUGIN_ROOT}/hooks/scope-lock-apply" ` (do **not** use the Write tool — it is blocked for `*.scope-lock` paths). The helper computes the manifest's sha256 and writes `.scope-lock` via shell redirection. 3. Commits both files (`chore: lock scope for (alignment passed)`). After the lock is in place, proceed to execution: diff --git a/skills/scope-lock/SKILL.md b/skills/scope-lock/SKILL.md index 2634c64..3e0edbd 100644 --- a/skills/scope-lock/SKILL.md +++ b/skills/scope-lock/SKILL.md @@ -104,7 +104,11 @@ The unlock path is intentionally heavyweight. Cheap unlock = no lock at all. **`alignment-check` (pre-lock and re-lock):** - After PASS, edit the plan's `**Status:**` line to `Locked `. -- Compute `sha256` of the manifest section (from `## Scope Manifest` to the next `##` heading) and write it to `.scope-lock`. This is the lock file. +- Write the lock file by running the helper via Bash (do **not** use the Write tool — the Write tool is blocked for `*.scope-lock` paths by the scope guard hook): + ``` + bash "${CLAUDE_PLUGIN_ROOT}/hooks/scope-lock-apply" + ``` + The helper extracts the `## Scope Manifest` section, computes its sha256, and writes `.scope-lock` via shell redirection. It prints the path and hash prefix on success, or an error message and exits non-zero on failure. - Commit both files in the same commit: `chore: lock scope for (alignment passed)`. **`subagent-driven-development` (per-task checkpoint):** From ed328673225bb20ec3b3603f2121a99717526cdc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 14:31:52 +0000 Subject: [PATCH 3/3] fix: clarify scope-lock write paths in pre-tool-scope-guard comments and break up long block message - Header comment now documents both override paths (agent: scope-lock-apply via Bash; operator: SUPERPOWERS_SCOPE_LOCK_WRITE=1) instead of mentioning only the new helper - Inline section comment updated to match, explicitly calling out both paths - Long block reason string broken into named-variable segments for readability and safer future edits Agent-Logs-Url: https://github.com/GoCodeAlone/claude-superpowers/sessions/b771bd78-7761-446e-b094-f26f0b0e2a38 Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- hooks/pre-tool-scope-guard | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/hooks/pre-tool-scope-guard b/hooks/pre-tool-scope-guard index 3b7c67f..a9c3e72 100755 --- a/hooks/pre-tool-scope-guard +++ b/hooks/pre-tool-scope-guard @@ -10,7 +10,8 @@ # — git push / gh pr create when locked plan hash mismatches # — git push / git commit to main or master (unless SUPERPOWERS_ALLOW_DEFAULT_BRANCH=1) # Write / Edit / MultiEdit tool -# — *.scope-lock files (use hooks/scope-lock-apply via Bash instead) +# — *.scope-lock files (agent path: hooks/scope-lock-apply via Bash; +# operator path: SUPERPOWERS_SCOPE_LOCK_WRITE=1) # — docs/plans/*.md when plan is Locked (unless SUPERPOWERS_PLAN_LOCK_WRITE=1) # # Global opt-out: set SUPERPOWERS_HOOKS_DISABLE=1 in the *operator's* terminal @@ -141,10 +142,23 @@ case "$tool_name" in while IFS= read -r fpath; do [ -z "$fpath" ] && continue - # ── 5. scope-lock files (always blocked unless sentinel env var) ───── + # ── 5. scope-lock files ────────────────────────────────────────────── + # Agent path: bash "${CLAUDE_PLUGIN_ROOT}/hooks/scope-lock-apply" + # Operator path (pre-session): export SUPERPOWERS_SCOPE_LOCK_WRITE=1 if printf '%s' "$fpath" | grep -qE '\.scope-lock$'; then if [ "${SUPERPOWERS_SCOPE_LOCK_WRITE:-}" != "1" ]; then - block "Writing to '$(basename "$fpath")' is blocked — .scope-lock files must be written by the scope-lock skill's helper, not via the Write tool. Run: bash \"\${CLAUDE_PLUGIN_ROOT}/hooks/scope-lock-apply\" . The helper extracts the Scope Manifest section, computes its sha256, and writes the lock file via shell redirection (which is not blocked). Direct Write/Edit calls break the manifest integrity guarantee and allow silent scope tampering. To update the lock legitimately after a scope reduction: go through the unlock path (recording-decisions → update manifest → re-run alignment-check → re-run scope-lock-apply)." + _reason="Writing to '$(basename "$fpath")' is blocked —" + _reason="${_reason} .scope-lock files must be written by the scope-lock skill's helper," + _reason="${_reason} not via the Write tool." + _reason="${_reason} Run: bash \"\${CLAUDE_PLUGIN_ROOT}/hooks/scope-lock-apply\" ." + _reason="${_reason} The helper extracts the Scope Manifest section, computes its sha256," + _reason="${_reason} and writes the lock file via shell redirection (which is not blocked)." + _reason="${_reason} Direct Write/Edit calls break the manifest integrity guarantee" + _reason="${_reason} and allow silent scope tampering." + _reason="${_reason} To update the lock legitimately after a scope reduction:" + _reason="${_reason} use the unlock path (recording-decisions → update manifest" + _reason="${_reason} → re-run alignment-check → re-run scope-lock-apply)." + block "$_reason" fi fi