claudepod 1.2.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/CHANGELOG.md +179 -50
- package/.devcontainer/CLAUDE.md +24 -7
- package/.devcontainer/README.md +2 -0
- package/.devcontainer/config/main-system-prompt.md +357 -81
- package/.devcontainer/config/settings.json +6 -3
- package/.devcontainer/devcontainer.json +17 -5
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +72 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +8 -2
- package/.devcontainer/features/lsp-servers/install.sh +25 -1
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +99 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/claudepod-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +16 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket:create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket:new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket:review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket:work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/scripts/setup-aliases.sh +41 -13
- package/.devcontainer/scripts/setup-plugins.sh +35 -13
- package/.devcontainer/scripts/setup.sh +1 -3
- package/README.md +37 -0
- package/package.json +1 -1
- package/.devcontainer/scripts/setup-lsp.sh +0 -20
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Auto-lint files after editing.
|
|
4
|
+
|
|
5
|
+
Reads tool input from stdin, detects file type by extension,
|
|
6
|
+
runs appropriate linter if available.
|
|
7
|
+
Outputs JSON with additionalContext containing lint warnings.
|
|
8
|
+
Non-blocking: exit 0 regardless of lint result.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Linter configuration: extension -> (command, args, name, parser)
|
|
18
|
+
PYTHON_EXTENSIONS = {".py", ".pyi"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def lint_python(file_path: str) -> tuple[bool, str]:
|
|
22
|
+
"""Run pyright on a Python file.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
(success, message)
|
|
26
|
+
"""
|
|
27
|
+
pyright_cmd = "pyright"
|
|
28
|
+
|
|
29
|
+
# Check if pyright is available
|
|
30
|
+
try:
|
|
31
|
+
subprocess.run(
|
|
32
|
+
["which", pyright_cmd],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
check=True
|
|
35
|
+
)
|
|
36
|
+
except subprocess.CalledProcessError:
|
|
37
|
+
return True, "" # Pyright not available
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
[pyright_cmd, "--outputjson", file_path],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=55
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Parse pyright JSON output
|
|
48
|
+
try:
|
|
49
|
+
output = json.loads(result.stdout)
|
|
50
|
+
diagnostics = output.get("generalDiagnostics", [])
|
|
51
|
+
|
|
52
|
+
if not diagnostics:
|
|
53
|
+
return True, "[Auto-linter] Pyright: No issues found"
|
|
54
|
+
|
|
55
|
+
# Format diagnostics
|
|
56
|
+
issues = []
|
|
57
|
+
for diag in diagnostics[:5]: # Limit to first 5 issues
|
|
58
|
+
severity = diag.get("severity", "info")
|
|
59
|
+
message = diag.get("message", "")
|
|
60
|
+
line = diag.get("range", {}).get("start", {}).get("line", 0) + 1
|
|
61
|
+
|
|
62
|
+
if severity == "error":
|
|
63
|
+
icon = "✗"
|
|
64
|
+
elif severity == "warning":
|
|
65
|
+
icon = "!"
|
|
66
|
+
else:
|
|
67
|
+
icon = "•"
|
|
68
|
+
|
|
69
|
+
issues.append(f" {icon} Line {line}: {message}")
|
|
70
|
+
|
|
71
|
+
total = len(diagnostics)
|
|
72
|
+
shown = min(5, total)
|
|
73
|
+
header = f"[Auto-linter] Pyright: {total} issue(s)"
|
|
74
|
+
if total > shown:
|
|
75
|
+
header += f" (showing first {shown})"
|
|
76
|
+
|
|
77
|
+
return True, header + "\n" + "\n".join(issues)
|
|
78
|
+
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
# Pyright output not JSON, might be an error
|
|
81
|
+
if result.stderr:
|
|
82
|
+
return True, f"[Auto-linter] Pyright error: {result.stderr.strip()[:100]}"
|
|
83
|
+
return True, ""
|
|
84
|
+
|
|
85
|
+
except subprocess.TimeoutExpired:
|
|
86
|
+
return True, "[Auto-linter] Pyright timed out"
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return True, f"[Auto-linter] Error: {e}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def lint_file(file_path: str) -> tuple[bool, str]:
|
|
92
|
+
"""Run appropriate linter for file.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
(success, message)
|
|
96
|
+
"""
|
|
97
|
+
ext = Path(file_path).suffix.lower()
|
|
98
|
+
|
|
99
|
+
if ext in PYTHON_EXTENSIONS:
|
|
100
|
+
return lint_python(file_path)
|
|
101
|
+
|
|
102
|
+
# No linter available for this file type
|
|
103
|
+
return True, ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
try:
|
|
108
|
+
input_data = json.load(sys.stdin)
|
|
109
|
+
tool_input = input_data.get("tool_input", {})
|
|
110
|
+
file_path = tool_input.get("file_path", "")
|
|
111
|
+
|
|
112
|
+
if not file_path:
|
|
113
|
+
sys.exit(0)
|
|
114
|
+
|
|
115
|
+
# Check if file exists
|
|
116
|
+
if not os.path.exists(file_path):
|
|
117
|
+
sys.exit(0)
|
|
118
|
+
|
|
119
|
+
_, message = lint_file(file_path)
|
|
120
|
+
|
|
121
|
+
if message:
|
|
122
|
+
# Output context for Claude
|
|
123
|
+
print(json.dumps({
|
|
124
|
+
"additionalContext": message
|
|
125
|
+
}))
|
|
126
|
+
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Hook error: {e}", file=sys.stderr)
|
|
133
|
+
sys.exit(0)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|
package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Block dangerous bash commands before execution",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Bash",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/block-dangerous.py",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Block dangerous bash commands before execution.
|
|
4
|
+
|
|
5
|
+
Reads tool input from stdin, checks against dangerous patterns.
|
|
6
|
+
Exit code 2 blocks the command with error message.
|
|
7
|
+
Exit code 0 allows the command to proceed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
DANGEROUS_PATTERNS = [
|
|
15
|
+
# Destructive filesystem deletion
|
|
16
|
+
(r'\brm\s+.*-[^\s]*r[^\s]*f[^\s]*\s+[/~](?:\s|$)',
|
|
17
|
+
"Blocked: rm -rf on root or home directory"),
|
|
18
|
+
(r'\brm\s+.*-[^\s]*f[^\s]*r[^\s]*\s+[/~](?:\s|$)',
|
|
19
|
+
"Blocked: rm -rf on root or home directory"),
|
|
20
|
+
(r'\brm\s+-rf\s+/(?:\s|$)',
|
|
21
|
+
"Blocked: rm -rf /"),
|
|
22
|
+
(r'\brm\s+-rf\s+~(?:\s|$)',
|
|
23
|
+
"Blocked: rm -rf ~"),
|
|
24
|
+
|
|
25
|
+
# Root-level file removal
|
|
26
|
+
(r'\bsudo\s+rm\b',
|
|
27
|
+
"Blocked: sudo rm - use caution with privileged deletion"),
|
|
28
|
+
|
|
29
|
+
# World-writable permissions
|
|
30
|
+
(r'\bchmod\s+777\b',
|
|
31
|
+
"Blocked: chmod 777 creates security vulnerability"),
|
|
32
|
+
(r'\bchmod\s+-R\s+777\b',
|
|
33
|
+
"Blocked: recursive chmod 777 creates security vulnerability"),
|
|
34
|
+
|
|
35
|
+
# Force push to main/master
|
|
36
|
+
(r'\bgit\s+push\s+.*--force.*\s+(origin\s+)?(main|master)\b',
|
|
37
|
+
"Blocked: force push to main/master destroys history"),
|
|
38
|
+
(r'\bgit\s+push\s+.*-f\s+.*\s+(origin\s+)?(main|master)\b',
|
|
39
|
+
"Blocked: force push to main/master destroys history"),
|
|
40
|
+
(r'\bgit\s+push\s+-f\s+(origin\s+)?(main|master)\b',
|
|
41
|
+
"Blocked: force push to main/master destroys history"),
|
|
42
|
+
(r'\bgit\s+push\s+--force\s+(origin\s+)?(main|master)\b',
|
|
43
|
+
"Blocked: force push to main/master destroys history"),
|
|
44
|
+
|
|
45
|
+
# System directory modification
|
|
46
|
+
(r'>\s*/usr/',
|
|
47
|
+
"Blocked: writing to /usr system directory"),
|
|
48
|
+
(r'>\s*/etc/',
|
|
49
|
+
"Blocked: writing to /etc system directory"),
|
|
50
|
+
(r'>\s*/bin/',
|
|
51
|
+
"Blocked: writing to /bin system directory"),
|
|
52
|
+
(r'>\s*/sbin/',
|
|
53
|
+
"Blocked: writing to /sbin system directory"),
|
|
54
|
+
|
|
55
|
+
# Disk formatting
|
|
56
|
+
(r'\bmkfs\.\w+',
|
|
57
|
+
"Blocked: disk formatting command"),
|
|
58
|
+
(r'\bdd\s+.*of=/dev/',
|
|
59
|
+
"Blocked: dd writing to device"),
|
|
60
|
+
|
|
61
|
+
# History manipulation
|
|
62
|
+
(r'\bgit\s+reset\s+--hard\s+origin/(main|master)\b',
|
|
63
|
+
"Blocked: hard reset to remote main/master - destructive operation"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_command(command: str) -> tuple[bool, str]:
|
|
68
|
+
"""Check if command matches any dangerous pattern.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
(is_dangerous, message)
|
|
72
|
+
"""
|
|
73
|
+
for pattern, message in DANGEROUS_PATTERNS:
|
|
74
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
75
|
+
return True, message
|
|
76
|
+
return False, ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
try:
|
|
81
|
+
input_data = json.load(sys.stdin)
|
|
82
|
+
tool_input = input_data.get("tool_input", {})
|
|
83
|
+
command = tool_input.get("command", "")
|
|
84
|
+
|
|
85
|
+
if not command:
|
|
86
|
+
sys.exit(0)
|
|
87
|
+
|
|
88
|
+
is_dangerous, message = check_command(command)
|
|
89
|
+
|
|
90
|
+
if is_dangerous:
|
|
91
|
+
# Output error message and exit 2 to block
|
|
92
|
+
print(json.dumps({
|
|
93
|
+
"error": message
|
|
94
|
+
}))
|
|
95
|
+
sys.exit(2)
|
|
96
|
+
|
|
97
|
+
# Allow command to proceed
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
except json.JSONDecodeError:
|
|
101
|
+
# If we can't parse input, allow by default
|
|
102
|
+
sys.exit(0)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
# Log error but don't block on hook failure
|
|
105
|
+
print(f"Hook error: {e}", file=sys.stderr)
|
|
106
|
+
sys.exit(0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Inject planning workflow reminder on user prompt submission",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"UserPromptSubmit": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "echo '{\"systemMessage\": \"<system-reminder>If the user is asking a question rather than requesting implementation, respond directly and offer to create a plan if relevant. For implementation requests: if no plan exists or the previous plan is complete, use EnterPlanMode to gather requirements and design an approach before writing code. Use AskUserQuestion to clarify ambiguities. Only implement after explicit user approval.</system-reminder>\"}'",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Block modifications to protected files",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Edit|Write",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/guard-protected.py",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Block modifications to protected files.
|
|
4
|
+
|
|
5
|
+
Reads tool input from stdin, checks file path against protected patterns.
|
|
6
|
+
Exit code 2 blocks the edit with error message.
|
|
7
|
+
Exit code 0 allows the edit to proceed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
# Patterns that should be protected from modification
|
|
15
|
+
PROTECTED_PATTERNS = [
|
|
16
|
+
# Environment secrets
|
|
17
|
+
(r'(^|/)\.env$', "Blocked: .env contains secrets - edit manually if needed"),
|
|
18
|
+
(r'(^|/)\.env\.[^/]+$', "Blocked: .env.* files contain secrets - edit manually if needed"),
|
|
19
|
+
|
|
20
|
+
# Git internals
|
|
21
|
+
(r'(^|/)\.git/', "Blocked: .git/ directory is managed by git"),
|
|
22
|
+
|
|
23
|
+
# Lock files (should be modified via package manager)
|
|
24
|
+
(r'(^|/)package-lock\.json$', "Blocked: package-lock.json - use npm install instead"),
|
|
25
|
+
(r'(^|/)yarn\.lock$', "Blocked: yarn.lock - use yarn install instead"),
|
|
26
|
+
(r'(^|/)pnpm-lock\.yaml$', "Blocked: pnpm-lock.yaml - use pnpm install instead"),
|
|
27
|
+
(r'(^|/)Gemfile\.lock$', "Blocked: Gemfile.lock - use bundle install instead"),
|
|
28
|
+
(r'(^|/)poetry\.lock$', "Blocked: poetry.lock - use poetry install instead"),
|
|
29
|
+
(r'(^|/)Cargo\.lock$', "Blocked: Cargo.lock - use cargo build instead"),
|
|
30
|
+
(r'(^|/)composer\.lock$', "Blocked: composer.lock - use composer install instead"),
|
|
31
|
+
(r'(^|/)uv\.lock$', "Blocked: uv.lock - use uv sync instead"),
|
|
32
|
+
|
|
33
|
+
# Certificates and keys
|
|
34
|
+
(r'\.pem$', "Blocked: .pem files contain sensitive cryptographic material"),
|
|
35
|
+
(r'\.key$', "Blocked: .key files contain sensitive cryptographic material"),
|
|
36
|
+
(r'\.crt$', "Blocked: .crt certificate files should not be edited directly"),
|
|
37
|
+
(r'\.p12$', "Blocked: .p12 files contain sensitive cryptographic material"),
|
|
38
|
+
(r'\.pfx$', "Blocked: .pfx files contain sensitive cryptographic material"),
|
|
39
|
+
|
|
40
|
+
# Credential files
|
|
41
|
+
(r'(^|/)credentials\.json$', "Blocked: credentials.json contains secrets"),
|
|
42
|
+
(r'(^|/)secrets\.yaml$', "Blocked: secrets.yaml contains secrets"),
|
|
43
|
+
(r'(^|/)secrets\.yml$', "Blocked: secrets.yml contains secrets"),
|
|
44
|
+
(r'(^|/)secrets\.json$', "Blocked: secrets.json contains secrets"),
|
|
45
|
+
(r'(^|/)\.secrets$', "Blocked: .secrets file contains secrets"),
|
|
46
|
+
|
|
47
|
+
# Auth directories and files
|
|
48
|
+
(r'(^|/)\.ssh/', "Blocked: .ssh/ contains sensitive authentication data"),
|
|
49
|
+
(r'(^|/)\.aws/', "Blocked: .aws/ contains AWS credentials"),
|
|
50
|
+
(r'(^|/)\.netrc$', "Blocked: .netrc contains authentication credentials"),
|
|
51
|
+
(r'(^|/)\.npmrc$', "Blocked: .npmrc may contain auth tokens - edit manually if needed"),
|
|
52
|
+
(r'(^|/)\.pypirc$', "Blocked: .pypirc contains PyPI credentials"),
|
|
53
|
+
|
|
54
|
+
# Other sensitive files
|
|
55
|
+
(r'(^|/)id_rsa', "Blocked: SSH private key file"),
|
|
56
|
+
(r'(^|/)id_ed25519', "Blocked: SSH private key file"),
|
|
57
|
+
(r'(^|/)id_ecdsa', "Blocked: SSH private key file"),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_path(file_path: str) -> tuple[bool, str]:
|
|
62
|
+
"""Check if file path matches any protected pattern.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
(is_protected, message)
|
|
66
|
+
"""
|
|
67
|
+
# Normalize path for consistent matching
|
|
68
|
+
normalized = file_path.replace('\\', '/')
|
|
69
|
+
|
|
70
|
+
for pattern, message in PROTECTED_PATTERNS:
|
|
71
|
+
if re.search(pattern, normalized, re.IGNORECASE):
|
|
72
|
+
return True, message
|
|
73
|
+
|
|
74
|
+
return False, ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
try:
|
|
79
|
+
input_data = json.load(sys.stdin)
|
|
80
|
+
tool_input = input_data.get("tool_input", {})
|
|
81
|
+
file_path = tool_input.get("file_path", "")
|
|
82
|
+
|
|
83
|
+
if not file_path:
|
|
84
|
+
sys.exit(0)
|
|
85
|
+
|
|
86
|
+
is_protected, message = check_path(file_path)
|
|
87
|
+
|
|
88
|
+
if is_protected:
|
|
89
|
+
# Output error message and exit 2 to block
|
|
90
|
+
print(json.dumps({
|
|
91
|
+
"error": message
|
|
92
|
+
}))
|
|
93
|
+
sys.exit(2)
|
|
94
|
+
|
|
95
|
+
# Allow edit to proceed
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
|
|
98
|
+
except json.JSONDecodeError:
|
|
99
|
+
# If we can't parse input, allow by default
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
# Log error but don't block on hook failure
|
|
103
|
+
print(f"Hook error: {e}", file=sys.stderr)
|
|
104
|
+
sys.exit(0)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|