claude-devkit-cli 1.2.4 → 1.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-devkit-cli",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "CLI toolkit for spec-first development with Claude Code — hooks, commands, guards, and test runners",
5
5
  "bin": {
6
6
  "claude-devkit": "./bin/devkit.js",
@@ -66,7 +66,11 @@ function main() {
66
66
 
67
67
  // Skip files outside the project directory (e.g. ~/.claude/plans/)
68
68
  const filePath = payload.tool_input?.file_path;
69
- if (filePath && !path.resolve(filePath).startsWith(process.cwd())) process.exit(0);
69
+ if (filePath) {
70
+ const projectDir = process.cwd() + path.sep;
71
+ const resolved = path.resolve(filePath);
72
+ if (!resolved.startsWith(projectDir) && resolved !== process.cwd()) process.exit(0);
73
+ }
70
74
 
71
75
  const oldStr = payload.tool_input?.old_string;
72
76
  const newStr = payload.tool_input?.new_string;
@@ -60,8 +60,9 @@ function main() {
60
60
  if (!filePath) process.exit(0);
61
61
 
62
62
  // Skip files outside the project directory (e.g. ~/.claude/plans/)
63
- const projectDir = process.cwd();
64
- if (!path.resolve(filePath).startsWith(projectDir)) process.exit(0);
63
+ const projectDir = process.cwd() + path.sep;
64
+ const resolvedFile = path.resolve(filePath);
65
+ if (!resolvedFile.startsWith(projectDir) && resolvedFile !== process.cwd()) process.exit(0);
65
66
 
66
67
  // Skip excluded patterns
67
68
  if (matchesExclude(filePath)) process.exit(0);
@@ -80,7 +81,6 @@ function main() {
80
81
  try {
81
82
  const stat = fs.statSync(filePath);
82
83
  if (stat.size > MAX_BYTES) {
83
- // >1MB = definitely over threshold, warn without exact count
84
84
  const rel = path.relative(process.cwd(), filePath);
85
85
  process.stdout.write(JSON.stringify({
86
86
  continue: true,
@@ -88,7 +88,7 @@ function main() {
88
88
  hookEventName: "PostToolUse",
89
89
  additionalContext: `Warning: ${rel} is ${Math.round(stat.size / 1024)}KB. Consider splitting into smaller modules.`,
90
90
  },
91
- }));
91
+ }) + "\n");
92
92
  process.exit(0);
93
93
  }
94
94
  const buf = fs.readFileSync(filePath);
@@ -112,7 +112,7 @@ function main() {
112
112
  hookEventName: "PostToolUse",
113
113
  additionalContext: warning,
114
114
  },
115
- })
115
+ }) + "\n"
116
116
  );
117
117
  }
118
118
 
@@ -19,42 +19,44 @@ set -euo pipefail
19
19
  INPUT=$(cat)
20
20
  [[ -z "$INPUT" ]] && exit 0
21
21
 
22
- # Check Node.js availability
23
- if ! command -v node &>/dev/null; then
24
- echo "WARNING: path-guard disabled Node.js not found." >&2
25
- exit 0
26
- fi
22
+ # Extract command from JSON — try node first, fall back to grep/sed
23
+ extract_command() {
24
+ if command -v node &>/dev/null; then
25
+ printf '%s' "$1" | node -e "
26
+ try {
27
+ const d = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
28
+ const cmd = d.tool_input?.command;
29
+ if (typeof cmd === 'string') process.stdout.write(cmd);
30
+ } catch {}
31
+ " 2>/dev/null
32
+ else
33
+ # Lightweight fallback: extract "command":"..." from JSON
34
+ printf '%s' "$1" | grep -o '"command":"[^"]*"' | head -1 | sed 's/^"command":"//;s/"$//' 2>/dev/null
35
+ fi
36
+ }
27
37
 
28
- # Parse JSON with inline Node.js (avoids jq dependency)
29
- COMMAND=$(printf '%s' "$INPUT" | node -e "
30
- try {
31
- const d = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
32
- const cmd = d.tool_input?.command;
33
- if (typeof cmd === 'string') process.stdout.write(cmd);
34
- else process.exit(0);
35
- } catch { process.exit(0); }
36
- " 2>/dev/null) || exit 0
38
+ COMMAND=$(extract_command "$INPUT") || exit 0
37
39
 
38
40
  [[ -z "$COMMAND" ]] && exit 0
39
41
 
40
42
  # ─── Blocked directory patterns ─────────────────────────────────────
41
43
 
42
- BLOCKED="node_modules"
43
- BLOCKED+="|__pycache__"
44
- BLOCKED+="|\.git/objects"
45
- BLOCKED+="|\.git/refs"
46
- BLOCKED+="|dist/"
47
- BLOCKED+="|build/"
44
+ # Use word boundaries (\b) and explicit path separators to avoid substring false positives
45
+ # e.g. "build/" should not match "rebuild/src" or "my-build-tool"
46
+ BLOCKED="(^|[ /])node_modules(/|$| )"
47
+ BLOCKED+="|(__pycache__)"
48
+ BLOCKED+="|\.git/(objects|refs)"
49
+ BLOCKED+="|(^|[ /])dist(/|$| )"
50
+ BLOCKED+="|(^|[ /])build(/|$| )"
48
51
  BLOCKED+="|\.next/"
49
- BLOCKED+="|vendor/"
50
- BLOCKED+="|Pods/"
52
+ BLOCKED+="|(^|[ /])vendor(/|$| )"
53
+ BLOCKED+="|(^|[ /])Pods(/|$| )"
51
54
  BLOCKED+="|\.build/"
52
55
  BLOCKED+="|DerivedData"
53
56
  BLOCKED+="|\.gradle/"
54
- BLOCKED+="|target/debug"
55
- BLOCKED+="|target/release"
57
+ BLOCKED+="|target/(debug|release)(/|$| )"
56
58
  BLOCKED+="|\.nuget"
57
- BLOCKED+="|\.cache"
59
+ BLOCKED+="|\.cache(/|$| )"
58
60
 
59
61
  # Append project-specific patterns from env
60
62
  if [[ -n "${PATH_GUARD_EXTRA:-}" ]]; then
@@ -7,7 +7,7 @@
7
7
  # Environment:
8
8
  # SELF_REVIEW_ENABLED — set to "false" to disable (default: true)
9
9
 
10
- set -euo pipefail
10
+ # No set -euo pipefail — this hook must NEVER fail
11
11
 
12
12
  # Check if disabled
13
13
  if [[ "${SELF_REVIEW_ENABLED:-true}" == "false" ]]; then
@@ -57,13 +57,9 @@ is_sensitive() {
57
57
  return 0 ;;
58
58
  serviceAccountKey.json|service-account*.json)
59
59
  return 0 ;;
60
- .mcp.json|config.json)
61
- # config.json only sensitive inside .docker/ — check full path
62
- if [[ "$basename" == "config.json" ]]; then
63
- [[ "$filepath" == *".docker/config.json"* ]] && return 0
64
- else
65
- return 0
66
- fi
60
+ config.json)
61
+ # config.json only sensitive inside .docker/
62
+ [[ "$filepath" == *".docker/config.json"* ]] && return 0
67
63
  ;;
68
64
  esac
69
65
 
@@ -151,11 +147,28 @@ warn_with_message() {
151
147
  exit 0
152
148
  }
153
149
 
150
+ # ─── Fast-path: skip obviously safe files ──────────────────────────
151
+
152
+ fast_path_safe() {
153
+ local ext="${1##*.}"
154
+ case "$ext" in
155
+ md|ts|tsx|js|jsx|css|scss|html|svg|json|yaml|yml|toml|xml|txt|sh|py|rb|rs|go|java|kt|swift|c|cpp|h|hpp|cs|vue|svelte|astro)
156
+ # But json could be sensitive — check name
157
+ if [[ "$ext" == "json" ]]; then
158
+ return 1 # not fast-path safe, need full check
159
+ fi
160
+ return 0 ;;
161
+ esac
162
+ return 1
163
+ }
164
+
154
165
  # ─── Check direct file access (Read/Write/Edit) → BLOCK ────────────
155
166
 
156
167
  if [[ -n "$FILE_PATH" ]]; then
157
- if is_sensitive "$FILE_PATH" || check_agentignore "$FILE_PATH"; then
158
- block_with_message "$FILE_PATH"
168
+ if ! fast_path_safe "$FILE_PATH"; then
169
+ if is_sensitive "$FILE_PATH" || check_agentignore "$FILE_PATH"; then
170
+ block_with_message "$FILE_PATH"
171
+ fi
159
172
  fi
160
173
  fi
161
174