chore(hooks): remove config-protection edit-block guardrail
Full removal per user request: the PreToolUse hook that blocked model Edit/Write on quality-gate files (settings.json, gitflow.sh, .githooks, doctor.sh, hooks, lib/tests, lint configs) plus its one-shot sentinel. - delete hooks/config-protection.sh - delete lib/tests/config-protection.test.sh - deregister the hook from settings.json (rtk-rewrite PreToolUse kept) - drop the README mention Residual protection unchanged: gitflow pre-commit guard + Gitea branch protection still block direct code commits to main/develop.
This commit is contained in:
@@ -23,7 +23,7 @@ claude-config/
|
|||||||
├── update-all.sh # One-command update for all components
|
├── update-all.sh # One-command update for all components
|
||||||
├── Makefile # Unified entry point: make install / doctor / update
|
├── Makefile # Unified entry point: make install / doctor / update
|
||||||
├── plugins.lock.json # Version pinning for non-marketplace dependencies
|
├── plugins.lock.json # Version pinning for non-marketplace dependencies
|
||||||
├── hooks/ # Session start, statusline, RTK rewrite, config-protection + design-toolchain guards
|
├── hooks/ # Session start, statusline, RTK rewrite + design-toolchain guards
|
||||||
├── agents/ # Execution units called by skills (never invoked directly)
|
├── agents/ # Execution units called by skills (never invoked directly)
|
||||||
├── skills/ # Entry points invoked via /skill-name
|
├── skills/ # Entry points invoked via /skill-name
|
||||||
├── skills-external/ # Vendored skill packs (gstack submodule + installer-fetched design packs)
|
├── skills-external/ # Vendored skill packs (gstack submodule + installer-fetched design packs)
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# config-protection.sh
|
|
||||||
#
|
|
||||||
# PreToolUse hook (Edit|Write|MultiEdit). Blocks edits to this config's
|
|
||||||
# quality-gate files — the guardrails an agent must not silently weaken to make
|
|
||||||
# an error "pass" (permission/hook registry, gitflow enforcement, the git
|
|
||||||
# pre-commit guard, the hooks themselves, the test suite, the health diagnostic,
|
|
||||||
# lint config). Exit 2 blocks the tool call and feeds the message back to the
|
|
||||||
# model (Claude Code PreToolUse contract).
|
|
||||||
#
|
|
||||||
# It fires only on the model's Edit/Write tool calls — never on shell-level file
|
|
||||||
# ops (the cp/ln in install.sh, link.sh), so bootstrap/deploy is unaffected.
|
|
||||||
#
|
|
||||||
# One-shot escape hatch: create .claude/.config-edit-ok (CWD-relative) with a
|
|
||||||
# NON-EMPTY reason inside; the hook logs the reason, consumes (rm) the sentinel,
|
|
||||||
# and allows that single edit. It never persists — a lingering sentinel would be
|
|
||||||
# a footgun. Discipline, per CLAUDE.global.md "Root causes only. No temp fixes.": fix
|
|
||||||
# the code, don't loosen the gate. Fails OPEN (exit 0) on parse failure so it can
|
|
||||||
# never wedge editing.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log="${HOME}/.claude/logs/config-protection.log"
|
|
||||||
sentinel="${PWD}/.claude/.config-edit-ok"
|
|
||||||
|
|
||||||
input="$(cat)"
|
|
||||||
path="$(printf '%s' "$input" \
|
|
||||||
| python3 -c 'import sys, json; print(json.load(sys.stdin).get("tool_input", {}).get("file_path", ""))' \
|
|
||||||
2>/dev/null || true)"
|
|
||||||
[ -z "$path" ] && exit 0
|
|
||||||
|
|
||||||
# Guardrail files, matched by path suffix (covers both the repo source and the
|
|
||||||
# deployed ~/.claude copy). Precise: lib/gitflow.sh only, not gitflow-migrate.sh.
|
|
||||||
case "$path" in
|
|
||||||
*/.claude/settings.json|*/.claude/settings.local.json|*/claude/settings.json) ;;
|
|
||||||
*/lib/gitflow.sh|*/.githooks/*|*/doctor.sh) ;;
|
|
||||||
*/hooks/*.sh|*/lib/tests/*) ;;
|
|
||||||
*/.shellcheckrc|*/.markdownlint.json|*/.editorconfig) ;;
|
|
||||||
*) exit 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# One-shot sentinel bypass: non-empty reason required; consumed on sight.
|
|
||||||
if [ -f "$sentinel" ]; then
|
|
||||||
reason="$(head -c 500 "$sentinel" 2>/dev/null | tr '\n\r\t' ' ' || true)"
|
|
||||||
rm -f "$sentinel"
|
|
||||||
if printf '%s' "$reason" | grep -q '[^[:space:]]'; then
|
|
||||||
mkdir -p "$(dirname "$log")"
|
|
||||||
printf '%s\tBYPASS\t%s\treason=%s\n' "$(date -Iseconds)" "$path" "$reason" >> "$log"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
printf '%s\n' "[config-protection] .claude/.config-edit-ok had an EMPTY reason -> refused (sentinel consumed). Recreate it with a non-empty reason." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat >&2 <<EOF
|
|
||||||
[config-protection] BLOCKED edit to a quality-gate file:
|
|
||||||
$path
|
|
||||||
This is a guardrail (permission/hook registry, gitflow enforcement, git
|
|
||||||
pre-commit guard, a hook, the test suite, health diagnostic, or lint config).
|
|
||||||
Don't weaken the gate to make an error pass — fix the root cause instead
|
|
||||||
(global CLAUDE.md: "Root causes only. No temp fixes."). To make one intended edit,
|
|
||||||
create .claude/.config-edit-ok with a non-empty reason; it is logged and
|
|
||||||
consumed (one-shot).
|
|
||||||
EOF
|
|
||||||
exit 2
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# lib/tests/config-protection.test.sh
|
|
||||||
set -u
|
|
||||||
H="$(cd "$(dirname "$0")/../.." && pwd)/hooks/config-protection.sh"
|
|
||||||
pass=0; fail=0
|
|
||||||
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
|
|
||||||
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
|
|
||||||
# Run hook for a file_path with NO sentinel present (CWD = a clean temp dir).
|
|
||||||
run() { local c r; c="$(mktemp -d)"; ( cd "$c" && printf \
|
|
||||||
'{"tool_name":"Edit","tool_input":{"file_path":"%s"}}' "$1" | bash "$H" ) \
|
|
||||||
>/dev/null 2>&1; r=$?; rm -rf "$c"; return "$r"; }
|
|
||||||
|
|
||||||
# --- Guarded quality-gate files -> blocked (exit 2) ---
|
|
||||||
run "/home/u/Documents/claude/lib/gitflow.sh"; check T1-gitflow "$?" 2
|
|
||||||
run "/home/u/.claude/settings.json"; check T2-live-settings "$?" 2
|
|
||||||
run "/home/u/Documents/claude/.claude/settings.local.json"; check T3-local-settings "$?" 2
|
|
||||||
run "/home/u/Documents/claude/settings.json"; check T4-root-settings "$?" 2
|
|
||||||
run "/home/u/Documents/claude/.githooks/pre-commit"; check T5-githook "$?" 2
|
|
||||||
run "/home/u/Documents/claude/doctor.sh"; check T6-doctor "$?" 2
|
|
||||||
run "/home/u/Documents/claude/.shellcheckrc"; check T7-shellcheckrc "$?" 2
|
|
||||||
# self-guard: the hook itself, other hooks, and the test suite are guarded
|
|
||||||
run "/home/u/Documents/claude/hooks/config-protection.sh"; check T8-self-guard "$?" 2
|
|
||||||
run "/home/u/.claude/hooks/session-start.sh"; check T9-deployed-hook "$?" 2
|
|
||||||
run "/home/u/Documents/claude/lib/tests/config-protection.test.sh"; check T10-tests-guarded "$?" 2
|
|
||||||
|
|
||||||
# --- Non-guarded -> allowed (exit 0) ---
|
|
||||||
run "/home/u/Documents/claude/lib/gitflow-migrate.sh"; check T11-near-miss "$?" 0
|
|
||||||
run "/home/u/project/src/app.js"; check T12-code "$?" 0
|
|
||||||
run "/home/u/project/settings.json"; check T13-foreign-settings "$?" 0
|
|
||||||
|
|
||||||
# --- Fail-open on malformed input (no file_path) -> allowed ---
|
|
||||||
c="$(mktemp -d)"; ( cd "$c" && printf '{}' | bash "$H" ) >/dev/null 2>&1
|
|
||||||
check T14-fail-open "$?" 0; rm -rf "$c"
|
|
||||||
|
|
||||||
# --- Sentinel one-shot: non-empty reason -> allow + log + consume; 2nd edit blocked ---
|
|
||||||
tmp="$(mktemp -d)"; mkdir -p "$tmp/.claude"
|
|
||||||
printf 'fixing eslint false-positive' > "$tmp/.claude/.config-edit-ok"
|
|
||||||
( cd "$tmp" && printf '{"tool_name":"Edit","tool_input":{"file_path":"/x/doctor.sh"}}' \
|
|
||||||
| HOME="$tmp" bash "$H" ) >/dev/null 2>&1
|
|
||||||
check T15-sentinel-allow "$?" 0
|
|
||||||
check T15-consumed "$([ -e "$tmp/.claude/.config-edit-ok" ] && echo present || echo gone)" gone
|
|
||||||
check T15-logged "$(grep -c 'BYPASS.*doctor.sh.*fixing eslint' \
|
|
||||||
"$tmp/.claude/logs/config-protection.log" 2>/dev/null)" 1
|
|
||||||
( cd "$tmp" && printf '{"tool_name":"Edit","tool_input":{"file_path":"/x/doctor.sh"}}' \
|
|
||||||
| HOME="$tmp" bash "$H" ) >/dev/null 2>&1
|
|
||||||
check T16-second-blocked "$?" 2
|
|
||||||
rm -rf "$tmp"
|
|
||||||
|
|
||||||
# --- Sentinel with EMPTY reason -> refused + consumed ---
|
|
||||||
tmp="$(mktemp -d)"; mkdir -p "$tmp/.claude"; : > "$tmp/.claude/.config-edit-ok"
|
|
||||||
( cd "$tmp" && printf '{"tool_name":"Edit","tool_input":{"file_path":"/x/doctor.sh"}}' \
|
|
||||||
| HOME="$tmp" bash "$H" ) >/dev/null 2>&1
|
|
||||||
check T17-empty-refused "$?" 2
|
|
||||||
check T17-consumed "$([ -e "$tmp/.claude/.config-edit-ok" ] && echo present || echo gone)" gone
|
|
||||||
rm -rf "$tmp"
|
|
||||||
|
|
||||||
# --- T18/T19: payload shapes beyond Edit (locks against future Edit-only narrowing) ---
|
|
||||||
c="$(mktemp -d)"; ( cd "$c" && printf \
|
|
||||||
'{"tool_name":"Write","tool_input":{"file_path":"/x/doctor.sh","content":"x"}}' | bash "$H" ) \
|
|
||||||
>/dev/null 2>&1; check T18-write-payload "$?" 2; rm -rf "$c"
|
|
||||||
|
|
||||||
c="$(mktemp -d)"; ( cd "$c" && printf \
|
|
||||||
'{"tool_name":"MultiEdit","tool_input":{"file_path":"/x/doctor.sh","edits":[{"old_string":"a","new_string":"b"}]}}' | bash "$H" ) \
|
|
||||||
>/dev/null 2>&1; check T19-multiedit-payload "$?" 2; rm -rf "$c"
|
|
||||||
|
|
||||||
# --- T20: sentinel with ONLY whitespace bytes (not literally empty) -> refused + consumed ---
|
|
||||||
tmp="$(mktemp -d)"; mkdir -p "$tmp/.claude"; printf ' \n\t' > "$tmp/.claude/.config-edit-ok"
|
|
||||||
( cd "$tmp" && printf '{"tool_name":"Edit","tool_input":{"file_path":"/x/doctor.sh"}}' \
|
|
||||||
| HOME="$tmp" bash "$H" ) >/dev/null 2>&1
|
|
||||||
check T20-whitespace-only-refused "$?" 2
|
|
||||||
check T20-whitespace-only-consumed "$([ -e "$tmp/.claude/.config-edit-ok" ] && echo present || echo gone)" gone
|
|
||||||
rm -rf "$tmp"
|
|
||||||
|
|
||||||
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
|
||||||
@@ -271,16 +271,6 @@
|
|||||||
"command": "bash ~/.claude/hooks/rtk-rewrite.sh"
|
"command": "bash ~/.claude/hooks/rtk-rewrite.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Edit|Write|MultiEdit",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash ~/.claude/hooks/config-protection.sh",
|
|
||||||
"timeout": 5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"UserPromptSubmit": [
|
"UserPromptSubmit": [
|
||||||
|
|||||||
Reference in New Issue
Block a user