Unverified 提交 446c953b authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add python permission hook (#2312)

#skip-bb <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add a Python permission hook that restricts python/python3 to scripts inside the .claude directory. Blocks unsafe modes and command injection to reduce risk. - **New Features** - Enforces: allow scripts under .claude; deny scripts outside; deny -m, -c, and interactive; passthrough for non-python and --version/--help. - Robust parsing of env-var prefixes, flags, quoted/unquoted paths, relative/absolute paths, and symlinks; supports CLAUDE_PROJECT_DIR. - Registered the hook in PreToolUse and expanded allowed tools in settings (Bash(chmod:*), Bash(python3:*)). Added tests for allowed, blocked, passthrough, and security-bypass commands. <sup>Written for commit 798d1abf04cdedc5395603ce4e32b2b943be8941. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude <noreply@anthropic.com>
上级 ff360a09
#!/usr/bin/env python3
"""
Python Permission Hook
This hook enforces that python/python3 commands can only execute scripts
located inside the .claude directory.
ALLOWED:
- python .claude/script.py
- python3 .claude/hooks/test.py
- python "$CLAUDE_PROJECT_DIR/.claude/script.py"
BLOCKED:
- python script.py (outside .claude)
- python /usr/local/bin/script.py
- python ../malicious.py
- python -m <module> (module execution bypasses directory restriction)
- python -c "<code>" (inline code execution)
- python < /tmp/file.py (stdin redirection)
- python .claude/script.py; malicious_command (shell injection)
PASSTHROUGH (normal permission flow):
- Non-python commands (ls, cat, etc.)
- python --version (version check)
- python --help (help)
"""
import json
import os
import re
import shlex
import sys
# Shell metacharacters that could allow command chaining/injection
# Based on gh-permission-hook.py patterns
SHELL_INJECTION_PATTERNS = re.compile(
r'('
r';' # Command separator
r'|(?<!\|)\|(?!\|)' # Single pipe (not ||)
r'|\|\|' # Logical OR
r'|&&' # Logical AND
r'|&\s+\S' # Background + another command
r'|&\S' # Background + another command
r'|&\s*$' # Trailing background operator
r'|`' # Backtick command substitution
r'|\$\(' # $( command substitution
r"|\$'" # ANSI-C quoting
r'|<\(' # Process substitution <(...)
r'|>\(' # Process substitution >(...)
r'|<<<' # Here-string
r'|<<[^<]' # Here-doc (<<EOF, <<'EOF', etc.)
r'|<\s*[^<]' # Input redirection (< file) - note: after heredoc checks
r'|\n' # Newline
r'|\r' # Carriage return
r')'
)
# Pattern to match single-quoted strings (safe to strip for metachar check)
SINGLE_QUOTED_PATTERN = re.compile(r"'[^']*'")
# Pattern to match double-quoted strings without command substitution
SAFE_DOUBLE_QUOTED_PATTERN = re.compile(r'"[^"$`]*"')
def contains_shell_injection(cmd: str) -> bool:
"""
Check if command contains shell metacharacters that could allow injection.
Returns True if dangerous patterns are found.
"""
# Strip single-quoted strings (truly safe in bash)
cmd_without_single_quotes = SINGLE_QUOTED_PATTERN.sub("''", cmd)
# Strip double-quoted strings that don't contain $( or backticks
cmd_without_safe_doubles = SAFE_DOUBLE_QUOTED_PATTERN.sub('""', cmd_without_single_quotes)
return bool(SHELL_INJECTION_PATTERNS.search(cmd_without_safe_doubles))
def is_python_command(cmd: str) -> bool:
"""
Quick check if a command looks like a python command.
Used to decide whether to apply python-specific security checks.
"""
# Strip env var prefixes
stripped = cmd.strip()
while True:
match = re.match(r'^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|\'[^\']*\'|[^\s]*)\s+', stripped)
if match:
stripped = stripped[match.end():]
else:
break
# Check for python pattern (including env python, path/to/python, etc.)
return bool(re.match(
r'^(?:env\s+)?(?:/usr/bin/env\s+)?(?:[^\s]*/)?python3?\b',
stripped
))
def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
# Invalid input, allow normal permission flow
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input")
# Validate types to prevent crashes on malformed input
if not isinstance(tool_input, dict):
sys.exit(0)
command = tool_input.get("command")
if not isinstance(command, str):
sys.exit(0)
# Only process Bash commands
if tool_name != "Bash":
sys.exit(0)
# Check if this is a python/python3 command
result = extract_python_script(command)
if result is None:
# Not a python command, let it through
sys.exit(0)
# Unpack result
script_path, denial_reason = result
# If there's a denial reason, deny the command
if denial_reason:
decision = make_deny_decision(denial_reason)
print(json.dumps(decision))
sys.exit(0)
# If script_path is empty string, it's a passthrough case (e.g., --version)
if script_path == "":
sys.exit(0)
# Check if the script is inside .claude directory
if is_inside_claude_dir(script_path):
decision = make_allow_decision(
f"Python script is inside .claude directory: {script_path}"
)
print(json.dumps(decision))
sys.exit(0)
else:
decision = make_deny_decision(
f"Python scripts can only be run from inside the .claude directory. "
f"Attempted to run: {script_path}"
)
print(json.dumps(decision))
sys.exit(0)
def extract_python_script(command: str) -> tuple[str, str] | None:
"""
Extract the Python script path from a command.
Returns:
- None if not a python command (passthrough to normal permission flow)
- (script_path, "") if a script was found that should be validated
- ("", "") if it's a passthrough case like --version or --help
- ("", denial_reason) if the command should be denied immediately
"""
cmd = command.strip()
# Check for shell injection FIRST before any parsing
if contains_shell_injection(command):
# Check if this even looks like a python command before denying
if is_python_command(cmd):
return ("", "Python command contains shell metacharacters that could allow injection")
# Not a python command, let normal flow handle it
return None
# Remove common environment variable prefixes
# e.g., "FOO=bar python script.py" -> "python script.py"
while True:
# Handle both unquoted and quoted env var values
match = re.match(r'^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|\'[^\']*\'|[^\s]*)\s+', cmd)
if match:
cmd = cmd[match.end():]
else:
break
# Check if command starts with python or python3
# Include: python, python3, /usr/bin/python, /usr/local/bin/python,
# and handle 'env python' patterns
python_match = re.match(
r'^(?:env\s+)?' # Optional 'env ' prefix
r'(?:/usr/bin/env\s+)?' # Optional '/usr/bin/env ' prefix
r'((?:[^\s]*/)?python3?)' # Python executable (with optional path)
r'(?:\s+|$)', # Followed by space or end of string
cmd
)
if not python_match:
return None
# Get the rest after "python" or "python3"
rest = cmd[python_match.end():].strip()
# If no arguments, it's interactive mode - DENY (stdin redirection risk)
if not rest:
return ("", "Interactive Python mode is not allowed (stdin redirection risk)")
# Use shlex for robust argument parsing
try:
args = shlex.split(rest)
except ValueError:
# Malformed quotes - deny for safety
return ("", "Malformed command (unmatched quotes)")
if not args:
return ("", "Interactive Python mode is not allowed (stdin redirection risk)")
i = 0
while i < len(args):
arg = args[i]
# Handle end-of-options delimiter
if arg == '--':
# Next argument is the script
if i + 1 < len(args):
return (args[i + 1], "")
return ("", "Interactive Python mode is not allowed (stdin redirection risk)")
# DENY: -m module execution (bypasses directory restriction)
# Check for both standalone -m and combined flags like -um, -Bm
if arg == '-m' or (arg.startswith('-') and not arg.startswith('--') and 'm' in arg[1:]):
return ("", "Python -m module execution is not allowed (bypasses directory restriction)")
# DENY: -c inline code execution
# Check for both standalone -c and combined flags like -Bc, -uc
if arg == '-c' or (arg.startswith('-') and not arg.startswith('--') and 'c' in arg[1:]):
return ("", "Python -c inline code execution is not allowed")
# Passthrough: version/help flags (safe, no code execution)
if arg in ('--version', '-V', '--help', '-h'):
return ("", "")
# Handle flags that take arguments
# Python 3 flags with arguments: -W (warning control), -X (implementation-specific options)
if arg in ('-W', '-X'):
i += 2 # Skip flag and its argument
continue
# Handle combined flags like -Werror or -Xdev
if arg.startswith('-W') or arg.startswith('-X'):
i += 1
continue
# Skip other short flags (e.g., -u, -B, -O, -OO, -s, -S, -E, -I)
if arg.startswith('-') and not arg.startswith('--'):
i += 1
continue
# Skip long options we don't specifically handle
if arg.startswith('--'):
i += 1
continue
# First non-flag argument is the script path
return (arg, "")
# Only flags, no script - passthrough for things like 'python --version'
return ("", "")
def is_inside_claude_dir(script_path: str) -> bool:
"""
Check if the script path is inside the .claude directory.
Handles both absolute and relative paths.
Security note: We intentionally expand environment variables to support
paths like $CLAUDE_PROJECT_DIR/.claude/script.py. The subsequent realpath()
call resolves the final path, and we verify it's inside .claude after
expansion. This prevents bypasses like $HOME/../../../tmp/malicious.py
because realpath() resolves to the actual location which is then checked.
"""
# Expand environment variables (see security note above)
expanded_path = os.path.expandvars(script_path)
# Get the project directory from environment or use current working directory
project_dir = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
claude_dir = os.path.join(project_dir, '.claude')
# Normalize the script path
if os.path.isabs(expanded_path):
abs_script_path = os.path.normpath(expanded_path)
else:
abs_script_path = os.path.normpath(os.path.join(project_dir, expanded_path))
# Resolve any symlinks to get the real path
try:
real_script_path = os.path.realpath(abs_script_path)
real_claude_dir = os.path.realpath(claude_dir)
except OSError:
# If we can't resolve paths, be conservative and deny
return False
# Check if the script is inside the .claude directory
# Use os.path.commonpath to handle edge cases
try:
common = os.path.commonpath([real_script_path, real_claude_dir])
return common == real_claude_dir
except ValueError:
# Different drives on Windows, etc.
return False
def make_allow_decision(reason: str) -> dict:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": reason
}
}
def make_deny_decision(reason: str) -> dict:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason
}
}
if __name__ == "__main__":
main()
# Commands that should be BLOCKED by the python-permission-hook
# Format: one command per line, lines starting with # are comments
# These are python commands that attempt to run scripts OUTSIDE the .claude directory
# =============================================================================
# SCRIPTS IN CURRENT DIRECTORY
# =============================================================================
python script.py
python3 script.py
python test.py
python3 test.py
python ./script.py
python3 ./script.py
# =============================================================================
# SCRIPTS IN OTHER DIRECTORIES
# =============================================================================
python src/script.py
python3 src/script.py
python src/test.py
python3 src/test.py
python lib/utils.py
python3 lib/utils.py
# =============================================================================
# ABSOLUTE PATHS OUTSIDE .claude
# =============================================================================
python /usr/local/bin/script.py
python3 /usr/local/bin/script.py
python /tmp/malicious.py
python3 /tmp/malicious.py
python /home/user/script.py
python3 /home/user/script.py
# =============================================================================
# PATH TRAVERSAL ATTEMPTS
# =============================================================================
python ../malicious.py
python3 ../malicious.py
python ../../escape.py
python3 ../../escape.py
python .claude/../escape.py
python3 .claude/../escape.py
# =============================================================================
# QUOTED PATHS OUTSIDE .claude
# =============================================================================
python "script.py"
python3 "script.py"
python 'script.py'
python3 'script.py'
python "/tmp/malicious.py"
python3 "/tmp/malicious.py"
# =============================================================================
# WITH FLAGS
# =============================================================================
python -u script.py
python3 -u script.py
python -B /tmp/malicious.py
python3 -B /tmp/malicious.py
# =============================================================================
# WITH ENVIRONMENT VARIABLES
# =============================================================================
FOO=bar python script.py
FOO=bar python3 script.py
DEBUG=1 python /tmp/test.py
DEBUG=1 python3 /tmp/test.py
# Commands that should be ALLOWED by the python-permission-hook
# Format: one command per line, lines starting with # are comments
# These are python commands that run scripts inside the .claude directory
# =============================================================================
# BASIC PYTHON COMMANDS
# =============================================================================
# Simple script execution
python .claude/script.py
python3 .claude/script.py
python .claude/test.py
python3 .claude/test.py
# Scripts in subdirectories
python .claude/hooks/test.py
python3 .claude/hooks/test.py
python .claude/hooks/tests/test.py
python3 .claude/hooks/tests/test.py
# =============================================================================
# QUOTED PATHS
# =============================================================================
python ".claude/script.py"
python3 ".claude/script.py"
python '.claude/script.py'
python3 '.claude/script.py'
python ".claude/hooks/test.py"
python3 ".claude/hooks/test.py"
# =============================================================================
# WITH FLAGS
# =============================================================================
python -u .claude/script.py
python3 -u .claude/script.py
python -B .claude/script.py
python3 -B .claude/script.py
python -u -B .claude/script.py
python3 -u -B .claude/script.py
# =============================================================================
# WITH ENVIRONMENT VARIABLES
# =============================================================================
FOO=bar python .claude/script.py
FOO=bar python3 .claude/script.py
DEBUG=1 python .claude/hooks/test.py
DEBUG=1 python3 .claude/hooks/test.py
FOO=bar BAZ=qux python .claude/script.py
PYTHONPATH=/some/path python .claude/script.py
# =============================================================================
# FULL PYTHON PATHS
# =============================================================================
/usr/bin/python .claude/script.py
/usr/bin/python3 .claude/script.py
/usr/local/bin/python .claude/script.py
/usr/local/bin/python3 .claude/script.py
# =============================================================================
# ENV PYTHON (various forms)
# =============================================================================
env python .claude/script.py
env python3 .claude/script.py
/usr/bin/env python .claude/script.py
/usr/bin/env python3 .claude/script.py
# =============================================================================
# VIRTUALENV / PYENV / CONDA PATHS
# =============================================================================
./venv/bin/python .claude/script.py
./venv/bin/python3 .claude/script.py
.venv/bin/python .claude/script.py
/home/user/.pyenv/shims/python .claude/script.py
/opt/conda/bin/python .claude/script.py
# =============================================================================
# FLAGS WITH ARGUMENTS (-W, -X)
# =============================================================================
python -W ignore .claude/script.py
python3 -W error .claude/script.py
python -X dev .claude/script.py
python3 -X utf8 .claude/script.py
python -W ignore -X dev .claude/script.py
# =============================================================================
# END-OF-OPTIONS DELIMITER (--)
# =============================================================================
python -- .claude/script.py
python3 -- .claude/script.py
python -u -- .claude/script.py
# Commands that should PASSTHROUGH (not be handled by python hook)
# These result in decision: 'none' - the hook doesn't make a decision
# Format: one command per line, lines starting with # are comments
# =============================================================================
# NON-PYTHON COMMANDS
# =============================================================================
# Basic shell commands
ls -la
cat file.txt
echo "hello"
npm install
git status
# Other interpreters
node script.js
ruby script.rb
perl script.pl
# =============================================================================
# PYTHON VERSION/HELP (passthrough to normal permission flow)
# =============================================================================
python --version
python3 --version
python -V
python3 -V
python --help
python3 --help
python -h
python3 -h
# Commands that should be BLOCKED due to security bypass attempts
# These test the new security features (shell injection, -m, -c, interactive mode)
# Format: one command per line, lines starting with # are comments
# =============================================================================
# SHELL INJECTION / COMMAND CHAINING
# =============================================================================
# Command separator
python .claude/script.py; malicious_command
python3 .claude/script.py; rm -rf /
# Logical AND
python .claude/script.py && malicious_command
python3 .claude/script.py && rm -rf /
# Logical OR
python .claude/script.py || malicious_command
python3 .claude/script.py || rm -rf /
# Pipe to another command
python .claude/script.py | cat
python3 .claude/script.py | sh
# Background + another command
python .claude/script.py & malicious_command
python3 .claude/script.py &rm -rf /
# Command substitution with $()
python .claude/script.py $(rm -rf /)
python3 .claude/script.py $(malicious_command)
# Command substitution with backticks
python .claude/script.py `rm -rf /`
python3 .claude/script.py `malicious_command`
# Process substitution
python <(echo "malicious code")
python3 <(cat /tmp/malicious.py)
# Input redirection (stdin)
python < /tmp/malicious.py
python3 < /tmp/script.py
python < script.py
python3 </tmp/malicious.py
# Newline command separation (encoded)
# Note: actual newlines in test file would break parsing
# =============================================================================
# MODULE EXECUTION (-m flag)
# =============================================================================
python -m http.server
python3 -m http.server
python -m pip install malicious_package
python3 -m pip install malicious_package
python -m runpy /tmp/malicious.py
python3 -m runpy script_outside_claude.py
python -m unittest discover
python3 -m pytest
# =============================================================================
# INLINE CODE EXECUTION (-c flag)
# =============================================================================
python -c "print('hello')"
python3 -c "print('hello')"
python -c 'import os; os.system("rm -rf /")'
python3 -c 'exec(open("/tmp/malicious.py").read())'
python -c "import subprocess; subprocess.call(['malicious'])"
python3 -c "__import__('os').system('whoami')"
# =============================================================================
# INTERACTIVE MODE (stdin redirection risk)
# =============================================================================
python
python3
# =============================================================================
# ENV PYTHON WITH DANGEROUS FLAGS
# =============================================================================
env python -m http.server
env python3 -c "print('hello')"
/usr/bin/env python -m pip install malicious
/usr/bin/env python3 -c "import os"
# =============================================================================
# HEREDOC / HERE-STRING ATTEMPTS
# =============================================================================
python <<EOF
python3 <<'EOF'
python <<<'print(1)'
python3 <<<"import os"
# =============================================================================
# COMBINED FLAGS BYPASS ATTEMPTS
# =============================================================================
# Combined -c flag (e.g., -Bc = -B + -c)
python -Bc "print('hello')"
python3 -Bc "import os"
python -uBc "malicious code"
python3 -OBc "__import__('os').system('whoami')"
# Combined -m flag (e.g., -um = -u + -m)
python -um http.server
python3 -Bm pip install malicious
python -uBm runpy /tmp/malicious.py
python3 -OBm pytest
#!/usr/bin/env python3
"""
Unit tests for python-permission-hook.py
This test loads commands from python_good_commands.txt and python_bad_commands.txt
and verifies that the hook correctly allows/denies them.
Run with: python .claude/hooks/tests/test_python_permission_hook.py
"""
import json
import subprocess
import sys
from pathlib import Path
def load_commands(filename: str) -> list[str]:
"""Load commands from a file, ignoring comments and empty lines."""
filepath = Path(__file__).parent / filename
commands = []
with open(filepath, "r") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith("#"):
commands.append(line)
return commands
def run_hook(command: str) -> dict:
"""
Run the permission hook with the given command and return the result.
Returns a dict with:
- 'decision': 'allow', 'deny', or 'none' (no decision/passthrough)
- 'reason': the reason string if a decision was made
"""
hook_path = Path(__file__).parent.parent / "python-permission-hook.py"
input_data = json.dumps({
"tool_name": "Bash",
"tool_input": {
"command": command
}
})
result = subprocess.run(
[sys.executable, str(hook_path)],
input=input_data,
capture_output=True,
text=True
)
if result.stdout.strip():
try:
output = json.loads(result.stdout.strip())
hook_output = output.get("hookSpecificOutput", {})
return {
"decision": hook_output.get("permissionDecision", "none"),
"reason": hook_output.get("permissionDecisionReason", "")
}
except json.JSONDecodeError:
return {"decision": "none", "reason": f"Invalid JSON output: {result.stdout}"}
return {"decision": "none", "reason": "No output (passthrough)"}
def test_good_commands() -> tuple[int, int, list[str]]:
"""Test that good commands are allowed."""
commands = load_commands("python_good_commands.txt")
passed = 0
failed = 0
failures = []
for cmd in commands:
result = run_hook(cmd)
# Good commands should be 'allow'
if result["decision"] != "allow":
failed += 1
failures.append(f" FAIL (not allowed): {cmd}\n Decision: {result['decision']}, Reason: {result['reason']}")
else:
passed += 1
return passed, failed, failures
def test_bad_commands() -> tuple[int, int, list[str]]:
"""Test that bad commands are denied."""
commands = load_commands("python_bad_commands.txt")
passed = 0
failed = 0
failures = []
for cmd in commands:
result = run_hook(cmd)
# Bad commands should be 'deny'
if result["decision"] != "deny":
failed += 1
failures.append(f" FAIL (not blocked): {cmd}\n Decision: {result['decision']}, Reason: {result['reason']}")
else:
passed += 1
return passed, failed, failures
def test_passthrough_commands() -> tuple[int, int, list[str]]:
"""Test that commands that should be ignored result in a passthrough."""
commands = load_commands("python_passthrough_commands.txt")
passed = 0
failed = 0
failures = []
for cmd in commands:
result = run_hook(cmd)
# These commands should result in a passthrough ('none')
if result["decision"] != "none":
failed += 1
failures.append(f" FAIL (not passthrough): {cmd}\n Decision: {result['decision']}, Reason: {result['reason']}")
else:
passed += 1
return passed, failed, failures
def test_security_blocked_commands() -> tuple[int, int, list[str]]:
"""Test that security bypass attempts are denied."""
commands = load_commands("python_security_blocked_commands.txt")
passed = 0
failed = 0
failures = []
for cmd in commands:
result = run_hook(cmd)
# Security bypass attempts should be 'deny'
if result["decision"] != "deny":
failed += 1
failures.append(f" FAIL (not blocked): {cmd}\n Decision: {result['decision']}, Reason: {result['reason']}")
else:
passed += 1
return passed, failed, failures
def main():
print("=" * 60)
print("Testing python-permission-hook.py")
print("=" * 60)
print()
# Test good commands
print("Testing GOOD commands (should be allowed)...")
good_passed, good_failed, good_failures = test_good_commands()
print(f" Passed: {good_passed}, Failed: {good_failed}")
if good_failures:
print("\n Failures:")
for failure in good_failures:
print(failure)
print()
# Test bad commands
print("Testing BAD commands (should be blocked)...")
bad_passed, bad_failed, bad_failures = test_bad_commands()
print(f" Passed: {bad_passed}, Failed: {bad_failed}")
if bad_failures:
print("\n Failures:")
for failure in bad_failures:
print(failure)
print()
# Test passthrough commands
print("Testing PASSTHROUGH commands (should not be handled by hook)...")
pass_passed, pass_failed, pass_failures = test_passthrough_commands()
print(f" Passed: {pass_passed}, Failed: {pass_failed}")
if pass_failures:
print("\n Failures:")
for failure in pass_failures:
print(failure)
print()
# Test security blocked commands
print("Testing SECURITY BLOCKED commands (bypass attempts should be denied)...")
sec_passed, sec_failed, sec_failures = test_security_blocked_commands()
print(f" Passed: {sec_passed}, Failed: {sec_failed}")
if sec_failures:
print("\n Failures:")
for failure in sec_failures:
print(failure)
print()
# Summary
print("=" * 60)
total_passed = good_passed + bad_passed + pass_passed + sec_passed
total_failed = good_failed + bad_failed + pass_failed + sec_failed
print(f"TOTAL: {total_passed} passed, {total_failed} failed")
print("=" * 60)
if total_failed > 0:
sys.exit(1)
else:
print("\nAll tests passed!")
sys.exit(0)
if __name__ == "__main__":
main()
...@@ -111,7 +111,11 @@ ...@@ -111,7 +111,11 @@
"Bash(sort:*)", "Bash(sort:*)",
"Bash(uniq:*)", "Bash(uniq:*)",
"Bash(cut:*)", "Bash(cut:*)",
"Bash(diff:*)" "Bash(diff:*)",
"Bash(chmod:*)",
"Bash(python3:*)"
] ]
}, },
"hooks": { "hooks": {
...@@ -123,6 +127,11 @@ ...@@ -123,6 +127,11 @@
"type": "command", "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/gh-permission-hook.py", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/gh-permission-hook.py",
"timeout": 5000 "timeout": 5000
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/python-permission-hook.py",
"timeout": 5000
} }
] ]
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论