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:
+22
-9
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user