@paulduvall/claude-dev-toolkit 0.0.1-alpha.2 → 0.0.1-alpha.21
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/LICENSE +21 -0
- package/README.md +88 -37
- package/bin/claude-commands +307 -65
- package/commands/active/xarchitecture.md +393 -0
- package/commands/active/xconfig.md +127 -0
- package/commands/active/xcontinue.md +92 -0
- package/commands/active/xdebug.md +130 -0
- package/commands/active/xdocs.md +178 -0
- package/commands/active/xexplore.md +94 -0
- package/commands/active/xgit.md +149 -0
- package/commands/active/xpipeline.md +152 -0
- package/commands/active/xquality.md +96 -0
- package/commands/active/xrefactor.md +198 -0
- package/commands/active/xrelease.md +142 -0
- package/commands/active/xsecurity.md +92 -0
- package/commands/active/xspec.md +174 -0
- package/commands/active/xtdd.md +151 -0
- package/commands/active/xtest.md +89 -0
- package/commands/active/xverify.md +80 -0
- package/commands/experiments/xact.md +742 -0
- package/commands/experiments/xanalytics.md +113 -0
- package/commands/experiments/xanalyze.md +70 -0
- package/commands/experiments/xapi.md +161 -0
- package/commands/experiments/xatomic.md +112 -0
- package/commands/experiments/xaws.md +85 -0
- package/commands/experiments/xcicd.md +337 -0
- package/commands/experiments/xcommit.md +122 -0
- package/commands/experiments/xcompliance.md +182 -0
- package/commands/experiments/xconstraints.md +89 -0
- package/commands/experiments/xcoverage.md +90 -0
- package/commands/experiments/xdb.md +102 -0
- package/commands/experiments/xdesign.md +121 -0
- package/commands/experiments/xdevcontainer.md +238 -0
- package/commands/experiments/xevaluate.md +111 -0
- package/commands/experiments/xfootnote.md +12 -0
- package/commands/experiments/xgenerate.md +117 -0
- package/commands/experiments/xgovernance.md +149 -0
- package/commands/experiments/xgreen.md +66 -0
- package/commands/experiments/xiac.md +118 -0
- package/commands/experiments/xincident.md +137 -0
- package/commands/experiments/xinfra.md +115 -0
- package/commands/experiments/xknowledge.md +115 -0
- package/commands/experiments/xmaturity.md +120 -0
- package/commands/experiments/xmetrics.md +118 -0
- package/commands/experiments/xmonitoring.md +128 -0
- package/commands/experiments/xnew.md +903 -0
- package/commands/experiments/xobservable.md +114 -0
- package/commands/experiments/xoidc.md +165 -0
- package/commands/experiments/xoptimize.md +115 -0
- package/commands/experiments/xperformance.md +112 -0
- package/commands/experiments/xplanning.md +131 -0
- package/commands/experiments/xpolicy.md +115 -0
- package/commands/experiments/xproduct.md +98 -0
- package/commands/experiments/xreadiness.md +75 -0
- package/commands/experiments/xred.md +55 -0
- package/commands/experiments/xrisk.md +128 -0
- package/commands/experiments/xrules.md +124 -0
- package/commands/experiments/xsandbox.md +120 -0
- package/commands/experiments/xscan.md +102 -0
- package/commands/experiments/xsetup.md +123 -0
- package/commands/experiments/xtemplate.md +116 -0
- package/commands/experiments/xtrace.md +212 -0
- package/commands/experiments/xux.md +171 -0
- package/commands/experiments/xvalidate.md +104 -0
- package/commands/experiments/xworkflow.md +113 -0
- package/hooks/.smellrc.example.json +19 -0
- package/hooks/README.md +263 -0
- package/hooks/check-commit-signing.py +127 -0
- package/hooks/check-complexity.py +38 -0
- package/hooks/check-security.py +37 -0
- package/hooks/claude-wrapper.sh +29 -0
- package/hooks/config.py +110 -0
- package/hooks/file-logger.sh +100 -0
- package/hooks/lib/argument-parser.sh +427 -0
- package/hooks/lib/config-constants.sh +230 -0
- package/hooks/lib/context-manager.sh +560 -0
- package/hooks/lib/error-handler.sh +423 -0
- package/hooks/lib/execution-engine.sh +444 -0
- package/hooks/lib/execution-results.sh +113 -0
- package/hooks/lib/execution-simulation.sh +114 -0
- package/hooks/lib/field-validators.sh +104 -0
- package/hooks/lib/file-utils.sh +398 -0
- package/hooks/lib/subagent-discovery.sh +468 -0
- package/hooks/lib/subagent-validator.sh +407 -0
- package/hooks/lib/validation-reporter.sh +134 -0
- package/hooks/on-error-debug.sh +226 -0
- package/hooks/pre-commit-quality.sh +204 -0
- package/hooks/pre-commit-test-runner.sh +132 -0
- package/hooks/pre-write-security.sh +115 -0
- package/hooks/prevent-credential-exposure.sh +279 -0
- package/hooks/security_bandit.py +177 -0
- package/hooks/security_checks.py +97 -0
- package/hooks/security_secrets.py +81 -0
- package/hooks/security_trojan.py +61 -0
- package/hooks/settings.example.json +52 -0
- package/hooks/smell_checks.py +238 -0
- package/hooks/smell_javascript.py +231 -0
- package/hooks/smell_python.py +110 -0
- package/hooks/smell_ruff.py +70 -0
- package/hooks/smell_types.py +72 -0
- package/hooks/subagent-trigger-simple.sh +202 -0
- package/hooks/subagent-trigger.sh +253 -0
- package/hooks/suppression.py +82 -0
- package/hooks/tab-color.sh +70 -0
- package/hooks/verify-before-edit.sh +135 -0
- package/lib/backup-restore-command.js +140 -0
- package/lib/base/base-command.js +252 -0
- package/lib/base/command-result.js +184 -0
- package/lib/config/constants.js +255 -0
- package/lib/config.js +48 -6
- package/lib/configure-command.js +428 -0
- package/lib/dependency-validator.js +64 -5
- package/lib/hook-installer-core.js +2 -2
- package/lib/installation-instruction-generator.js +213 -495
- package/lib/installer.js +134 -56
- package/lib/oidc-command.js +740 -0
- package/lib/services/backup-list-service.js +226 -0
- package/lib/services/backup-service.js +230 -0
- package/lib/services/command-installer-service.js +217 -0
- package/lib/services/logger-service.js +201 -0
- package/lib/services/package-manager-service.js +319 -0
- package/lib/services/platform-instruction-service.js +294 -0
- package/lib/services/recovery-instruction-service.js +348 -0
- package/lib/services/restore-service.js +221 -0
- package/lib/setup-command.js +359 -0
- package/lib/setup-wizard.js +155 -262
- package/lib/uninstall-command.js +100 -0
- package/lib/utils/claude-path-config.js +184 -0
- package/lib/utils/file-system-utils.js +152 -0
- package/lib/utils.js +8 -4
- package/lib/verify-command.js +430 -0
- package/package.json +7 -3
- package/scripts/postinstall.js +172 -157
- package/subagents/debug-specialist.md +7 -0
- package/templates/README.md +115 -0
- package/templates/basic-settings.json +30 -0
- package/templates/comprehensive-settings.json +57 -0
- package/templates/global-claude.md +344 -0
- package/templates/hybrid-hook-config.yaml +132 -0
- package/templates/security-focused-settings.json +62 -0
- package/templates/subagent-hooks.yaml +188 -0
- package/lib/package-manager-service.js +0 -270
- package/subagents/debug-context.md +0 -197
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse hook: verify GPG/SSH commit signing before git commit.
|
|
3
|
+
|
|
4
|
+
Reads a PreToolUse JSON event from stdin. If the Bash command is a
|
|
5
|
+
git commit, verifies that commit signing is configured. Blocks with
|
|
6
|
+
remediation instructions if not.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
_GIT_COMMIT_RE = re.compile(r"\bgit\s+commit\b")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_git_commit(command: str) -> bool:
|
|
20
|
+
"""Return True if command contains a git commit invocation."""
|
|
21
|
+
return bool(_GIT_COMMIT_RE.search(command))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _git_config(key: str) -> str:
|
|
25
|
+
"""Read a git config value, returning empty string on failure."""
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["git", "config", "--get", key],
|
|
29
|
+
capture_output=True, text=True, timeout=5,
|
|
30
|
+
)
|
|
31
|
+
return result.stdout.strip()
|
|
32
|
+
except (subprocess.SubprocessError, OSError):
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _check_gpg_signing() -> str | None:
|
|
37
|
+
"""Verify GPG signing config. Return error message or None."""
|
|
38
|
+
gpg_format = _git_config("gpg.format")
|
|
39
|
+
if gpg_format == "ssh":
|
|
40
|
+
return _check_ssh_signing()
|
|
41
|
+
program = _git_config("gpg.program")
|
|
42
|
+
if not program:
|
|
43
|
+
return "gpg.program is not configured"
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_ssh_signing() -> str | None:
|
|
48
|
+
"""Verify SSH signing config. Return error message or None."""
|
|
49
|
+
allowed = _git_config("gpg.ssh.allowedSignersFile")
|
|
50
|
+
if not allowed:
|
|
51
|
+
return "gpg.ssh.allowedSignersFile is not configured"
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_REMEDIATION_TEMPLATE = """\
|
|
56
|
+
Commit signing is not properly configured: {error}
|
|
57
|
+
|
|
58
|
+
IMPORTANT: Ask the user which option they prefer before proceeding.
|
|
59
|
+
|
|
60
|
+
**Option 1 -- GPG signing** (traditional, widely supported):
|
|
61
|
+
```
|
|
62
|
+
git config --global commit.gpgsign true
|
|
63
|
+
git config --global gpg.program gpg
|
|
64
|
+
```
|
|
65
|
+
Requires: GPG installed, a GPG key generated (`gpg --gen-key`)
|
|
66
|
+
|
|
67
|
+
**Option 2 -- SSH signing** (simpler, uses existing SSH keys):
|
|
68
|
+
|
|
69
|
+
Step 1 -- Generate an SSH key (skip if ~/.ssh/id_ed25519.pub exists):
|
|
70
|
+
```
|
|
71
|
+
ssh-keygen -t ed25519 -C "your_email@example.com"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Step 2 -- Configure git to use SSH signing:
|
|
75
|
+
```
|
|
76
|
+
git config --global commit.gpgsign true
|
|
77
|
+
git config --global gpg.format ssh
|
|
78
|
+
git config --global user.signingkey ~/.ssh/id_ed25519.pub
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Step 3 -- Create the allowed signers file:
|
|
82
|
+
```
|
|
83
|
+
echo "$(git config --get user.email) namespaces=\\"git\\" $(cat ~/.ssh/id_ed25519.pub)" > ~/.ssh/allowed_signers
|
|
84
|
+
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Step 4 -- Add the key to GitHub as a **signing key**:
|
|
88
|
+
```
|
|
89
|
+
gh ssh-key add ~/.ssh/id_ed25519.pub --type signing
|
|
90
|
+
```
|
|
91
|
+
(Or manually: GitHub -> Settings -> SSH and GPG keys -> New SSH key -> \
|
|
92
|
+
Key type: **Signing Key**)
|
|
93
|
+
|
|
94
|
+
Ask the user: "Commit signing isn't configured. Would you like me to \
|
|
95
|
+
set it up for you? I can configure GPG or SSH signing -- which do you \
|
|
96
|
+
prefer?" Then run the commands for their chosen option.\
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_remediation(error: str) -> str:
|
|
101
|
+
"""Build a user-friendly remediation message."""
|
|
102
|
+
return _REMEDIATION_TEMPLATE.format(error=error)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
"""Entry point: check commit signing config before git commit."""
|
|
107
|
+
try:
|
|
108
|
+
event = json.load(sys.stdin)
|
|
109
|
+
except (json.JSONDecodeError, EOFError):
|
|
110
|
+
sys.exit(0)
|
|
111
|
+
command = event.get("tool_input", {}).get("command", "")
|
|
112
|
+
if not _is_git_commit(command):
|
|
113
|
+
sys.exit(0)
|
|
114
|
+
gpgsign = _git_config("commit.gpgsign")
|
|
115
|
+
if gpgsign != "true":
|
|
116
|
+
reason = _build_remediation("commit.gpgsign is not enabled")
|
|
117
|
+
print(json.dumps({"decision": "block", "reason": reason}))
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
error = _check_gpg_signing()
|
|
120
|
+
if error:
|
|
121
|
+
reason = _build_remediation(error)
|
|
122
|
+
print(json.dumps({"decision": "block", "reason": reason}))
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook: detect common code smells in modified files.
|
|
3
|
+
|
|
4
|
+
Delegates all analysis to smell_checks module. Reads the PostToolUse
|
|
5
|
+
JSON event from stdin, checks the written file, and emits a blocking
|
|
6
|
+
result when violations are found.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# Allow importing sibling module from the same directory.
|
|
16
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
17
|
+
|
|
18
|
+
from smell_checks import check_file, format_violations # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
"""Entry point: read PostToolUse event, check file, emit result."""
|
|
23
|
+
try:
|
|
24
|
+
event = json.load(sys.stdin)
|
|
25
|
+
except (json.JSONDecodeError, EOFError):
|
|
26
|
+
sys.exit(0)
|
|
27
|
+
file_path = event.get("tool_input", {}).get("file_path", "")
|
|
28
|
+
if not file_path or not os.path.isfile(file_path):
|
|
29
|
+
sys.exit(0)
|
|
30
|
+
smells = check_file(file_path)
|
|
31
|
+
if smells:
|
|
32
|
+
reason = format_violations(file_path, smells)
|
|
33
|
+
print(json.dumps({"decision": "block", "reason": reason}))
|
|
34
|
+
sys.exit(0)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook: detect security violations in modified files.
|
|
3
|
+
|
|
4
|
+
Delegates analysis to security_checks module. Reads the PostToolUse
|
|
5
|
+
JSON event from stdin, checks the written file, and emits a blocking
|
|
6
|
+
result when violations are found.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
|
|
17
|
+
from security_checks import check_security, format_security_violations # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
"""Entry point: read PostToolUse event, check file, emit result."""
|
|
22
|
+
try:
|
|
23
|
+
event = json.load(sys.stdin)
|
|
24
|
+
except (json.JSONDecodeError, EOFError):
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
file_path = event.get("tool_input", {}).get("file_path", "")
|
|
27
|
+
if not file_path or not os.path.isfile(file_path):
|
|
28
|
+
sys.exit(0)
|
|
29
|
+
smells = check_security(file_path)
|
|
30
|
+
if smells:
|
|
31
|
+
reason = format_security_violations(file_path, smells)
|
|
32
|
+
print(json.dumps({"decision": "block", "reason": reason}))
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# claude-wrapper.sh - Shell wrapper for claude that manages iTerm2 tab colors
|
|
3
|
+
#
|
|
4
|
+
# Source this file in ~/.zshrc:
|
|
5
|
+
# source ~/Code/claude-code/hooks/claude-wrapper.sh
|
|
6
|
+
#
|
|
7
|
+
# Sets gray tab color on launch, red on non-zero exit, then resets.
|
|
8
|
+
# Mid-session colors (blue=working, green=done) are handled by Claude Code hooks.
|
|
9
|
+
# See ~/.claude/settings.json and tab-color.sh
|
|
10
|
+
|
|
11
|
+
CCDK_HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
12
|
+
|
|
13
|
+
claude() {
|
|
14
|
+
# Set initial tab color to gray (idle, no prompt running yet)
|
|
15
|
+
"$CCDK_HOOKS_DIR/tab-color.sh" gray < /dev/null
|
|
16
|
+
|
|
17
|
+
# Pass all args through to the real claude binary
|
|
18
|
+
command claude "$@"
|
|
19
|
+
local exit_code=$?
|
|
20
|
+
|
|
21
|
+
if [ $exit_code -ne 0 ]; then
|
|
22
|
+
"$CCDK_HOOKS_DIR/tab-color.sh" red < /dev/null
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Reset tab color to default
|
|
26
|
+
"$CCDK_HOOKS_DIR/tab-color.sh" reset < /dev/null
|
|
27
|
+
|
|
28
|
+
return $exit_code
|
|
29
|
+
}
|
package/hooks/config.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Per-project configuration loader for code smell and security hooks.
|
|
2
|
+
|
|
3
|
+
Searches for .smellrc.json walking up from the target file's directory.
|
|
4
|
+
Falls back to defaults matching the hardcoded thresholds in smell_types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import fnmatch
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Config:
|
|
18
|
+
"""Immutable configuration for all hooks."""
|
|
19
|
+
|
|
20
|
+
max_complexity: int = 10
|
|
21
|
+
max_function_lines: int = 20
|
|
22
|
+
max_nesting_depth: int = 3
|
|
23
|
+
max_parameters: int = 4
|
|
24
|
+
max_file_lines: int = 300
|
|
25
|
+
duplicate_min_lines: int = 4
|
|
26
|
+
security_enabled: bool = True
|
|
27
|
+
trojan_enabled: bool = True
|
|
28
|
+
suppress_files: tuple[str, ...] = ()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEFAULT_CONFIG = Config()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find_config_file(start_dir: str) -> str | None:
|
|
35
|
+
"""Walk up from start_dir looking for .smellrc.json."""
|
|
36
|
+
current = os.path.abspath(start_dir)
|
|
37
|
+
root = os.path.dirname(current)
|
|
38
|
+
while current != root:
|
|
39
|
+
candidate = os.path.join(current, ".smellrc.json")
|
|
40
|
+
if os.path.isfile(candidate):
|
|
41
|
+
return candidate
|
|
42
|
+
root = current
|
|
43
|
+
current = os.path.dirname(current)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_config(path: str) -> Config:
|
|
48
|
+
"""Parse a .smellrc.json file into a Config object."""
|
|
49
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
50
|
+
raw = json.load(fh)
|
|
51
|
+
thresholds = raw.get("thresholds", {})
|
|
52
|
+
security = raw.get("security", {})
|
|
53
|
+
suppress = raw.get("suppress_files", [])
|
|
54
|
+
return Config(
|
|
55
|
+
max_complexity=thresholds.get("max_complexity", DEFAULT_CONFIG.max_complexity),
|
|
56
|
+
max_function_lines=thresholds.get("max_function_lines", DEFAULT_CONFIG.max_function_lines),
|
|
57
|
+
max_nesting_depth=thresholds.get("max_nesting_depth", DEFAULT_CONFIG.max_nesting_depth),
|
|
58
|
+
max_parameters=thresholds.get("max_parameters", DEFAULT_CONFIG.max_parameters),
|
|
59
|
+
max_file_lines=thresholds.get("max_file_lines", DEFAULT_CONFIG.max_file_lines),
|
|
60
|
+
duplicate_min_lines=thresholds.get("duplicate_min_lines", DEFAULT_CONFIG.duplicate_min_lines),
|
|
61
|
+
security_enabled=security.get("enabled", DEFAULT_CONFIG.security_enabled),
|
|
62
|
+
trojan_enabled=security.get("trojan_enabled", DEFAULT_CONFIG.trojan_enabled),
|
|
63
|
+
suppress_files=tuple(suppress),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@lru_cache(maxsize=32)
|
|
68
|
+
def _cached_load(config_path: str) -> Config:
|
|
69
|
+
"""Load and cache a config file by its resolved path."""
|
|
70
|
+
return _parse_config(config_path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_config(file_path: str) -> Config:
|
|
74
|
+
"""Load config for a given source file.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
file_path: Path to the source file being checked.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Config from nearest .smellrc.json, or DEFAULT_CONFIG.
|
|
81
|
+
"""
|
|
82
|
+
start = os.path.dirname(os.path.abspath(file_path))
|
|
83
|
+
config_path = _find_config_file(start)
|
|
84
|
+
if config_path is None:
|
|
85
|
+
return DEFAULT_CONFIG
|
|
86
|
+
try:
|
|
87
|
+
return _cached_load(config_path)
|
|
88
|
+
except (json.JSONDecodeError, OSError, KeyError, TypeError):
|
|
89
|
+
return DEFAULT_CONFIG
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_file_suppressed(file_path: str, config: Config) -> bool:
|
|
93
|
+
"""Check if a file matches any suppress_files glob patterns.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
file_path: Path to check against suppression globs.
|
|
97
|
+
config: Config with suppress_files patterns.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if the file should be skipped.
|
|
101
|
+
"""
|
|
102
|
+
if not config.suppress_files:
|
|
103
|
+
return False
|
|
104
|
+
basename = os.path.basename(file_path)
|
|
105
|
+
for pattern in config.suppress_files:
|
|
106
|
+
if fnmatch.fnmatch(basename, pattern):
|
|
107
|
+
return True
|
|
108
|
+
if fnmatch.fnmatch(file_path, pattern):
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Claude Code Hook: File Logger
|
|
5
|
+
#
|
|
6
|
+
# Purpose: Simple demonstration of hook functionality
|
|
7
|
+
# Trigger: PreToolUse for Edit, Write, MultiEdit tools
|
|
8
|
+
# Blocking: No - just logs activity
|
|
9
|
+
#
|
|
10
|
+
# This hook demonstrates basic hook functionality by logging file operations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
##################################
|
|
14
|
+
# Configuration
|
|
15
|
+
##################################
|
|
16
|
+
HOOK_NAME="file-logger"
|
|
17
|
+
LOG_FILE="$HOME/.claude/logs/file-logger.log"
|
|
18
|
+
|
|
19
|
+
# Ensure log directory exists with secure permissions
|
|
20
|
+
mkdir -p "$(dirname "$LOG_FILE")"
|
|
21
|
+
chmod 700 "$(dirname "$LOG_FILE")"
|
|
22
|
+
|
|
23
|
+
# Create log file with restrictive permissions if it doesn't exist
|
|
24
|
+
touch "$LOG_FILE"
|
|
25
|
+
chmod 600 "$LOG_FILE"
|
|
26
|
+
|
|
27
|
+
##################################
|
|
28
|
+
# Logging Functions
|
|
29
|
+
##################################
|
|
30
|
+
log() {
|
|
31
|
+
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$HOOK_NAME] $*" | tee -a "$LOG_FILE"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
##################################
|
|
35
|
+
# Dependency Validation
|
|
36
|
+
##################################
|
|
37
|
+
validate_hook_dependencies() {
|
|
38
|
+
local deps=("wc" "file")
|
|
39
|
+
local missing=()
|
|
40
|
+
|
|
41
|
+
for dep in "${deps[@]}"; do
|
|
42
|
+
if ! command -v "$dep" &> /dev/null; then
|
|
43
|
+
missing+=("$dep")
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
48
|
+
log "ERROR: Missing required dependencies: ${missing[*]}"
|
|
49
|
+
echo "Install missing tools and retry"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
##################################
|
|
55
|
+
# Main Hook Logic
|
|
56
|
+
##################################
|
|
57
|
+
main() {
|
|
58
|
+
# Validate dependencies first
|
|
59
|
+
validate_hook_dependencies
|
|
60
|
+
local tool_name="${CLAUDE_TOOL:-unknown}"
|
|
61
|
+
local file_path="${CLAUDE_FILE:-unknown}"
|
|
62
|
+
|
|
63
|
+
log "Hook triggered!"
|
|
64
|
+
log "Tool: $tool_name"
|
|
65
|
+
log "File: $file_path"
|
|
66
|
+
|
|
67
|
+
# Only process file modification tools
|
|
68
|
+
case "$tool_name" in
|
|
69
|
+
"Edit"|"Write"|"MultiEdit")
|
|
70
|
+
log "Processing file modification tool: $tool_name"
|
|
71
|
+
;;
|
|
72
|
+
*)
|
|
73
|
+
log "Skipping non-file tool: $tool_name"
|
|
74
|
+
exit 0
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
|
|
78
|
+
# Get basic file info if file exists
|
|
79
|
+
if [[ -n "$file_path" ]] && [[ "$file_path" != "unknown" ]] && [[ -f "$file_path" ]]; then
|
|
80
|
+
local file_size
|
|
81
|
+
file_size=$(wc -c < "$file_path" 2>/dev/null || echo "0")
|
|
82
|
+
local file_lines
|
|
83
|
+
file_lines=$(wc -l < "$file_path" 2>/dev/null || echo "0")
|
|
84
|
+
|
|
85
|
+
log "File size: $file_size bytes"
|
|
86
|
+
log "File lines: $file_lines"
|
|
87
|
+
log "File type: $(file -b "$file_path" 2>/dev/null || echo "unknown")"
|
|
88
|
+
else
|
|
89
|
+
log "File does not exist yet or path unknown"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Always allow the operation to proceed
|
|
93
|
+
log "Operation allowed - no blocking behavior"
|
|
94
|
+
exit 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
##################################
|
|
98
|
+
# Execute Main Function
|
|
99
|
+
##################################
|
|
100
|
+
main "$@"
|