Add opt-in raw-git nudge hook, independent of anti-chaining hook

Forces a normal confirmation prompt (never a silent block, never a silent
allow) when Bash runs raw git instead of git_cmd.sh, worded more
insistently when the subcommand is one the wrapper already supports.
Raw git stays a valid fallback for unsupported subcommands and
unregistered repos - a hard deny would remove that fallback entirely.

Also documents in SKILL.md that repo ambiguity (which registered repo is
meant, or none at all) must be resolved by asking the user rather than
guessing - a hook only sees the literal command string, not the
conversation, so that judgment can't live in the hook itself.
This commit is contained in:
Henner M. Kruse
2026-08-04 13:58:30 +00:00
parent 47595ff160
commit 1d0b66df2e
5 changed files with 173 additions and 19 deletions
+22 -9
View File
@@ -12,7 +12,7 @@
#
# Usage:
# setup.sh --settings-scope (project|user) [--project-dir <path>]
# [--repo <name>=<path> ...] [--install-hook]
# [--repo <name>=<path> ...] [--install-hook] [--install-raw-git-hook]
#
# Prints a JSON summary of what changed to stdout at the end.
@@ -22,6 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
SKILL_DIR="$(realpath "$PLUGIN_DIR/skills/git-manager")"
SOURCE_HOOK_PATH="$PLUGIN_DIR/hooks/force-ask-on-chaining.sh"
SOURCE_RAW_GIT_HOOK_PATH="$PLUGIN_DIR/hooks/force-ask-on-raw-git.sh"
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: python3 is required." >&2
@@ -37,10 +38,12 @@ mkdir -p "$STABLE_DIR"
cp "$SKILL_DIR/scripts/git_cmd.sh" "$STABLE_DIR/git_cmd.sh"
cp "$SKILL_DIR/scripts/_lib.sh" "$STABLE_DIR/_lib.sh"
cp "$SOURCE_HOOK_PATH" "$STABLE_DIR/force-ask-on-chaining.sh"
chmod +x "$STABLE_DIR/git_cmd.sh" "$STABLE_DIR/force-ask-on-chaining.sh"
cp "$SOURCE_RAW_GIT_HOOK_PATH" "$STABLE_DIR/force-ask-on-raw-git.sh"
chmod +x "$STABLE_DIR/git_cmd.sh" "$STABLE_DIR/force-ask-on-chaining.sh" "$STABLE_DIR/force-ask-on-raw-git.sh"
WRAPPER_PATH="$STABLE_DIR/git_cmd.sh"
HOOK_PATH="$STABLE_DIR/force-ask-on-chaining.sh"
RAW_GIT_HOOK_PATH="$STABLE_DIR/force-ask-on-raw-git.sh"
# Tilde-form paths for permission rules and SKILL.md's example commands —
# Claude Code matches Bash permission rules against the literal,
# unexpanded command text, so a rule written with the resolved absolute
@@ -55,6 +58,7 @@ TILDE_CONFIG_FILE="~/.agent-skills/git-manager/config.json"
SETTINGS_SCOPE=""
PROJECT_DIR="$PWD"
INSTALL_HOOK=0
INSTALL_RAW_GIT_HOOK=0
REPOS=()
while [ $# -gt 0 ]; do
@@ -63,6 +67,7 @@ while [ $# -gt 0 ]; do
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
--repo) REPOS+=("$2"); shift 2 ;;
--install-hook) INSTALL_HOOK=1; shift ;;
--install-raw-git-hook) INSTALL_RAW_GIT_HOOK=1; shift ;;
*) echo "Error: unknown argument '$1'" >&2; exit 1 ;;
esac
done
@@ -89,11 +94,12 @@ mkdir -p "$CONFIG_DIR"
# parsed arguments. ---
mkdir -p "$(dirname "$SETTINGS_FILE")"
python3 - "$SETTINGS_FILE" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" "$HOOK_PATH" "$INSTALL_HOOK" << 'PYEOF'
python3 - "$SETTINGS_FILE" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" "$HOOK_PATH" "$INSTALL_HOOK" "$RAW_GIT_HOOK_PATH" "$INSTALL_RAW_GIT_HOOK" << 'PYEOF'
import json, os, sys
settings_file, tilde_wrapper_path, tilde_config_file, hook_path, install_hook = sys.argv[1:6]
settings_file, tilde_wrapper_path, tilde_config_file, hook_path, install_hook, raw_git_hook_path, install_raw_git_hook = sys.argv[1:8]
install_hook = install_hook == "1"
install_raw_git_hook = install_raw_git_hook == "1"
settings = {}
if os.path.exists(settings_file):
@@ -114,12 +120,12 @@ for rule in new_rules:
if rule not in allow:
allow.append(rule)
if install_hook:
def install_pre_tool_use_hook(path):
hooks = settings.setdefault("hooks", {})
pre_tool_use = hooks.setdefault("PreToolUse", [])
already_present = any(
entry.get("matcher") == "Bash"
and any(h.get("command") == hook_path for h in entry.get("hooks", []))
and any(h.get("command") == path for h in entry.get("hooks", []))
for entry in pre_tool_use
)
if not already_present:
@@ -128,9 +134,15 @@ if install_hook:
# Real absolute path here (not tilde) — this is executed
# directly by Claude Code as a subprocess, not matched as text,
# so it needs to be an actually-invokable path.
"hooks": [{"type": "command", "command": hook_path}],
"hooks": [{"type": "command", "command": path}],
})
if install_hook:
install_pre_tool_use_hook(hook_path)
if install_raw_git_hook:
install_pre_tool_use_hook(raw_git_hook_path)
with open(settings_file, "w") as f:
json.dump(settings, f, indent=2)
f.write("\n")
@@ -171,14 +183,15 @@ with open(config_file, "w") as f:
PYEOF
# --- 5. Summary ---
python3 - "$STABLE_DIR" "$SETTINGS_FILE" "$CONFIG_FILE" "$INSTALL_HOOK" "$REPOS_JSON" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" << 'PYEOF'
python3 - "$STABLE_DIR" "$SETTINGS_FILE" "$CONFIG_FILE" "$INSTALL_HOOK" "$REPOS_JSON" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" "$INSTALL_RAW_GIT_HOOK" << 'PYEOF'
import json, sys
stable_dir, settings_file, config_file, install_hook, repos_json, tilde_wrapper_path, tilde_config_file = sys.argv[1:8]
stable_dir, settings_file, config_file, install_hook, repos_json, tilde_wrapper_path, tilde_config_file, install_raw_git_hook = sys.argv[1:9]
print(json.dumps({
"stable_script_location": stable_dir,
"settings_file": settings_file,
"config_file": config_file,
"hook_installed": install_hook == "1",
"raw_git_hook_installed": install_raw_git_hook == "1",
"repos_registered": json.loads(repos_json),
"permission_rules_added": [f"Bash({tilde_wrapper_path}:*)", f"Read({tilde_config_file})"],
"note": "The permission rule points at a stable location this script controls (~/.agent-skills/git-manager/bin/), not at Claude Code's internal plugin cache — so it survives future plugin updates without changing. It's written in tilde form (~/...) to match the literal, unexpanded command text Claude Code matches against, not the resolved absolute $HOME path. Permissions are written to disk now, but Claude Code loads permissions at session start and does not always pick up changes made by its own file edits within the same session — if a git_cmd.sh call still prompts after this, start a fresh session rather than re-running setup."