gsd-cc 1.2.1 → 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/bin/install.js CHANGED
@@ -155,9 +155,89 @@ function install(isGlobal) {
155
155
  fs.chmodSync(autoLoop, 0o755);
156
156
  }
157
157
 
158
+ // 5. Install hooks
159
+ const hooksSrc = path.join(__dirname, '..', 'hooks');
160
+ const hooksDest = path.join(skillsBase, 'gsd-cc-shared', 'hooks');
161
+ if (fs.existsSync(hooksSrc)) {
162
+ copyDir(hooksSrc, hooksDest);
163
+ // Make hooks executable
164
+ const hookFiles = fs.readdirSync(hooksDest);
165
+ for (const f of hookFiles) {
166
+ fs.chmodSync(path.join(hooksDest, f), 0o755);
167
+ }
168
+ fileCount += hookFiles.length;
169
+ }
170
+
171
+ // 6. Configure hooks in settings.json
172
+ installHooks(isGlobal, hooksDest);
173
+
158
174
  console.log(` ${green}✓${reset} Installed ${fileCount} files to ${label}`);
159
175
  }
160
176
 
177
+ /**
178
+ * Install hooks into .claude/settings.json or .claude/settings.local.json
179
+ */
180
+ function installHooks(isGlobal, hooksDir) {
181
+ const settingsPath = isGlobal
182
+ ? path.join(os.homedir(), '.claude', 'settings.json')
183
+ : path.join(process.cwd(), '.claude', 'settings.local.json');
184
+
185
+ let settings = {};
186
+ if (fs.existsSync(settingsPath)) {
187
+ try {
188
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
189
+ } catch (e) {
190
+ settings = {};
191
+ }
192
+ }
193
+
194
+ if (!settings.hooks) settings.hooks = {};
195
+
196
+ const boundaryGuard = path.join(hooksDir, 'gsd-boundary-guard.sh');
197
+ const promptGuard = path.join(hooksDir, 'gsd-prompt-guard.sh');
198
+ const contextMonitor = path.join(hooksDir, 'gsd-context-monitor.sh');
199
+ const workflowGuard = path.join(hooksDir, 'gsd-workflow-guard.sh');
200
+ const statusline = path.join(hooksDir, 'gsd-statusline.sh');
201
+
202
+ // Remove all existing GSD-CC hooks before adding (idempotent)
203
+ for (const event of Object.keys(settings.hooks)) {
204
+ settings.hooks[event] = settings.hooks[event].filter(
205
+ h => !JSON.stringify(h).includes('gsd-')
206
+ );
207
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
208
+ }
209
+
210
+ // PreToolUse: boundary guard + prompt injection guard on Edit/Write
211
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
212
+ settings.hooks.PreToolUse.push({
213
+ matcher: 'Edit|Write',
214
+ hooks: [
215
+ { type: 'command', command: boundaryGuard, timeout: 5000 },
216
+ { type: 'command', command: promptGuard, timeout: 5000 }
217
+ ]
218
+ });
219
+
220
+ // PostToolUse: context monitor (all tools) + workflow guard (Edit/Write)
221
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
222
+ settings.hooks.PostToolUse.push({
223
+ hooks: [{ type: 'command', command: contextMonitor, timeout: 5000 }]
224
+ });
225
+ settings.hooks.PostToolUse.push({
226
+ matcher: 'Edit|Write',
227
+ hooks: [{ type: 'command', command: workflowGuard, timeout: 5000 }]
228
+ });
229
+
230
+ // Notification: statusline
231
+ if (!settings.hooks.Notification) settings.hooks.Notification = [];
232
+ settings.hooks.Notification.push({
233
+ hooks: [{ type: 'command', command: statusline, timeout: 3000 }]
234
+ });
235
+
236
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
237
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
238
+ console.log(` ${green}✓${reset} Hooks configured in ${settingsPath.replace(os.homedir(), '~').replace(process.cwd(), '.')}`);
239
+ }
240
+
161
241
  /**
162
242
  * Write language to CLAUDE.md
163
243
  */
@@ -254,9 +334,32 @@ function uninstall() {
254
334
  }
255
335
  }
256
336
 
337
+ // Clean up hooks from settings files
338
+ for (const isGlobal of [true, false]) {
339
+ const settingsPath = isGlobal
340
+ ? path.join(os.homedir(), '.claude', 'settings.json')
341
+ : path.join(process.cwd(), '.claude', 'settings.local.json');
342
+ if (fs.existsSync(settingsPath)) {
343
+ try {
344
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
345
+ if (settings.hooks) {
346
+ for (const event of Object.keys(settings.hooks)) {
347
+ settings.hooks[event] = settings.hooks[event].filter(
348
+ h => !JSON.stringify(h).includes('gsd-')
349
+ );
350
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
351
+ }
352
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
353
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
354
+ }
355
+ } catch (e) { /* ignore parse errors */ }
356
+ }
357
+ }
358
+
257
359
  if (!removed) {
258
360
  console.log(` ${yellow}No GSD-CC installation found.${reset}`);
259
361
  } else {
362
+ console.log(` ${green}✓${reset} Hooks removed from settings`);
260
363
  console.log(`\n ${green}Done.${reset} GSD-CC has been removed.`);
261
364
  }
262
365
  }
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # GSD-CC Boundary Guard — PreToolUse hook
3
+ # Blocks Write/Edit operations on files listed in .gsd/STATE.md boundaries.
4
+ # This is a HARD enforcement — Claude cannot bypass this regardless of prompting.
5
+
6
+ INPUT=$(cat)
7
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
8
+ CWD=$(echo "$INPUT" | jq -r '.cwd')
9
+
10
+ # Only check Edit and Write operations
11
+ if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
12
+ exit 0
13
+ fi
14
+
15
+ # Get the file being edited/written
16
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
17
+ if [ -z "$FILE_PATH" ]; then
18
+ exit 0
19
+ fi
20
+
21
+ # Check if STATE.md exists and has boundaries
22
+ STATE_FILE="$CWD/.gsd/STATE.md"
23
+ if [ ! -f "$STATE_FILE" ]; then
24
+ exit 0
25
+ fi
26
+
27
+ # Extract boundary files from STATE.md
28
+ # Boundaries section contains lines like: - path/to/file (reason)
29
+ BOUNDARIES=$(sed -n '/^## Boundaries Active/,/^##/p' "$STATE_FILE" | grep '^ *- ' | sed 's/^ *- //' | sed 's/ (.*//')
30
+
31
+ if [ -z "$BOUNDARIES" ]; then
32
+ exit 0
33
+ fi
34
+
35
+ # Normalize the target file path (make relative to CWD if absolute)
36
+ RELATIVE_PATH="$FILE_PATH"
37
+ if [[ "$FILE_PATH" == "$CWD"* ]]; then
38
+ RELATIVE_PATH="${FILE_PATH#$CWD/}"
39
+ fi
40
+
41
+ # Check each boundary file
42
+ while IFS= read -r BOUNDARY_FILE; do
43
+ BOUNDARY_FILE=$(echo "$BOUNDARY_FILE" | xargs) # trim whitespace
44
+ if [ -z "$BOUNDARY_FILE" ]; then
45
+ continue
46
+ fi
47
+
48
+ # Check exact match or path containment
49
+ if [ "$RELATIVE_PATH" = "$BOUNDARY_FILE" ] || [ "$FILE_PATH" = "$BOUNDARY_FILE" ]; then
50
+ jq -n --arg file "$BOUNDARY_FILE" '{
51
+ "hookSpecificOutput": {
52
+ "hookEventName": "PreToolUse",
53
+ "permissionDecision": "deny",
54
+ "permissionDecisionReason": ("BOUNDARY VIOLATION: " + $file + " is in the DO NOT CHANGE list for this task. This file is protected. If you need to modify it, stop and discuss with the user first.")
55
+ }
56
+ }'
57
+ exit 0
58
+ fi
59
+ done <<< "$BOUNDARIES"
60
+
61
+ # File not in boundaries — allow
62
+ exit 0
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # GSD-CC Context Monitor — PostToolUse hook
3
+ # Injects warnings when context usage is getting high.
4
+ # Uses the transcript file size as a proxy for context consumption.
5
+
6
+ INPUT=$(cat)
7
+ CWD=$(echo "$INPUT" | jq -r '.cwd')
8
+ TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
9
+
10
+ # Skip if no transcript path
11
+ if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then
12
+ exit 0
13
+ fi
14
+
15
+ # Check if we're in a GSD-CC project
16
+ if [ ! -d "$CWD/.gsd" ]; then
17
+ exit 0
18
+ fi
19
+
20
+ # Use transcript line count as context proxy
21
+ # Typical context window: ~200K tokens ≈ ~2000 transcript lines for a heavy session
22
+ LINE_COUNT=$(wc -l < "$TRANSCRIPT" | xargs)
23
+
24
+ # Debounce: only warn every 20 tool calls
25
+ DEBOUNCE_FILE="/tmp/gsd-cc-ctx-monitor-$$"
26
+ if [ -f "$DEBOUNCE_FILE" ]; then
27
+ LAST_WARN=$(cat "$DEBOUNCE_FILE")
28
+ DIFF=$((LINE_COUNT - LAST_WARN))
29
+ if [ "$DIFF" -lt 50 ]; then
30
+ exit 0
31
+ fi
32
+ fi
33
+
34
+ # Warning thresholds (based on transcript lines)
35
+ if [ "$LINE_COUNT" -gt 1500 ]; then
36
+ echo "$LINE_COUNT" > "$DEBOUNCE_FILE"
37
+ jq -n '{
38
+ "hookSpecificOutput": {
39
+ "hookEventName": "PostToolUse",
40
+ "additionalContext": "⚠️ CRITICAL: Context window is very full. You MUST wrap up the current task immediately, write the summary, and instruct the user to start a fresh session. Do NOT start new work."
41
+ }
42
+ }'
43
+ exit 0
44
+ elif [ "$LINE_COUNT" -gt 1000 ]; then
45
+ echo "$LINE_COUNT" > "$DEBOUNCE_FILE"
46
+ jq -n '{
47
+ "hookSpecificOutput": {
48
+ "hookEventName": "PostToolUse",
49
+ "additionalContext": "⚠️ WARNING: Context window is filling up. Finish the current task soon and prepare to hand off to a fresh session."
50
+ }
51
+ }'
52
+ exit 0
53
+ fi
54
+
55
+ exit 0
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # GSD-CC Prompt Injection Guard — PreToolUse hook
3
+ # Scans Write/Edit operations targeting .gsd/ files for prompt injection patterns.
4
+ # Blocks suspicious content from being written into planning artifacts.
5
+
6
+ INPUT=$(cat)
7
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
8
+ CWD=$(echo "$INPUT" | jq -r '.cwd')
9
+
10
+ # Only check Edit and Write operations
11
+ if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
12
+ exit 0
13
+ fi
14
+
15
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
16
+ if [ -z "$FILE_PATH" ]; then
17
+ exit 0
18
+ fi
19
+
20
+ # Only scan writes to .gsd/ directory (planning artifacts)
21
+ if [[ "$FILE_PATH" != *".gsd/"* ]]; then
22
+ exit 0
23
+ fi
24
+
25
+ # Get the content being written
26
+ if [ "$TOOL_NAME" = "Write" ]; then
27
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
28
+ elif [ "$TOOL_NAME" = "Edit" ]; then
29
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty')
30
+ fi
31
+
32
+ if [ -z "$CONTENT" ]; then
33
+ exit 0
34
+ fi
35
+
36
+ # Check for prompt injection patterns
37
+ SUSPICIOUS=false
38
+ REASON=""
39
+
40
+ # Pattern 1: Direct instruction override attempts
41
+ if echo "$CONTENT" | grep -iqE 'ignore (previous|prior|above|all) (instructions|prompts|rules)'; then
42
+ SUSPICIOUS=true
43
+ REASON="Detected 'ignore previous instructions' pattern"
44
+ fi
45
+
46
+ # Pattern 2: Role reassignment
47
+ if echo "$CONTENT" | grep -iqE '(you are now|act as|pretend to be|your new role|forget your|disregard your)'; then
48
+ SUSPICIOUS=true
49
+ REASON="Detected role reassignment pattern"
50
+ fi
51
+
52
+ # Pattern 3: System prompt extraction
53
+ if echo "$CONTENT" | grep -iqE '(show|reveal|print|output|display) (your|the|system) (prompt|instructions|rules)'; then
54
+ SUSPICIOUS=true
55
+ REASON="Detected system prompt extraction attempt"
56
+ fi
57
+
58
+ # Pattern 4: Invisible Unicode characters (zero-width spaces, RTL override, etc.)
59
+ if echo "$CONTENT" | grep -qP '[\x{200B}\x{200C}\x{200D}\x{FEFF}\x{202A}-\x{202E}\x{2066}-\x{2069}]' 2>/dev/null; then
60
+ SUSPICIOUS=true
61
+ REASON="Detected invisible Unicode characters"
62
+ fi
63
+
64
+ # Pattern 5: Base64-encoded instructions
65
+ if echo "$CONTENT" | grep -qE '[A-Za-z0-9+/]{50,}={0,2}'; then
66
+ # Only flag if it's in a suspicious context (not normal code)
67
+ if echo "$CONTENT" | grep -iqE '(decode|eval|execute|base64).*[A-Za-z0-9+/]{50,}'; then
68
+ SUSPICIOUS=true
69
+ REASON="Detected potentially encoded instructions"
70
+ fi
71
+ fi
72
+
73
+ # Pattern 6: HTML/script injection in markdown
74
+ if echo "$CONTENT" | grep -iqE '<script|javascript:|on(load|error|click)='; then
75
+ SUSPICIOUS=true
76
+ REASON="Detected script injection in planning artifact"
77
+ fi
78
+
79
+ if [ "$SUSPICIOUS" = true ]; then
80
+ jq -n --arg reason "$REASON" --arg file "$FILE_PATH" '{
81
+ "hookSpecificOutput": {
82
+ "hookEventName": "PreToolUse",
83
+ "permissionDecision": "deny",
84
+ "permissionDecisionReason": ("PROMPT INJECTION BLOCKED: " + $reason + " in " + $file + ". This content cannot be written to planning artifacts. If this is a false positive, the user can write the file manually.")
85
+ }
86
+ }'
87
+ exit 0
88
+ fi
89
+
90
+ # Content is clean
91
+ exit 0
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # GSD-CC Statusline — Notification hook
3
+ # Renders project status in the terminal statusline.
4
+ # Shows: current phase, milestone/slice/task position, and project type.
5
+
6
+ INPUT=$(cat)
7
+ CWD=$(echo "$INPUT" | jq -r '.cwd')
8
+
9
+ # Only render if this is a GSD-CC project
10
+ STATE_FILE="$CWD/.gsd/STATE.md"
11
+ if [ ! -f "$STATE_FILE" ]; then
12
+ exit 0
13
+ fi
14
+
15
+ # Parse STATE.md frontmatter
16
+ PHASE=$(grep '^phase:' "$STATE_FILE" | head -1 | sed 's/phase: *//')
17
+ MILESTONE=$(grep '^milestone:' "$STATE_FILE" | head -1 | sed 's/milestone: *//')
18
+ SLICE=$(grep '^current_slice:' "$STATE_FILE" | head -1 | sed 's/current_slice: *//')
19
+ TASK=$(grep '^current_task:' "$STATE_FILE" | head -1 | sed 's/current_task: *//')
20
+ RIGOR=$(grep '^rigor:' "$STATE_FILE" | head -1 | sed 's/rigor: *//')
21
+
22
+ # Build position string
23
+ POSITION="$MILESTONE"
24
+ if [ "$SLICE" != "—" ] && [ -n "$SLICE" ]; then
25
+ POSITION="$POSITION / $SLICE"
26
+ fi
27
+ if [ "$TASK" != "—" ] && [ -n "$TASK" ]; then
28
+ POSITION="$POSITION / $TASK"
29
+ fi
30
+
31
+ # Map phase to display name
32
+ case "$PHASE" in
33
+ "seed") PHASE_DISPLAY="Seed" ;;
34
+ "seed-complete") PHASE_DISPLAY="Seed ✓" ;;
35
+ "stack-complete") PHASE_DISPLAY="Stack ✓" ;;
36
+ "roadmap-complete") PHASE_DISPLAY="Roadmap ✓" ;;
37
+ "discuss-complete") PHASE_DISPLAY="Discuss ✓" ;;
38
+ "plan-complete") PHASE_DISPLAY="Plan ✓" ;;
39
+ "planning") PHASE_DISPLAY="Planning..." ;;
40
+ "applying") PHASE_DISPLAY="Executing..." ;;
41
+ "apply-complete") PHASE_DISPLAY="UNIFY required" ;;
42
+ "unified") PHASE_DISPLAY="Unified ✓" ;;
43
+ *) PHASE_DISPLAY="$PHASE" ;;
44
+ esac
45
+
46
+ # Count progress
47
+ TOTAL_SLICES=$(grep -c '| S[0-9]' "$STATE_FILE" 2>/dev/null || echo "0")
48
+ DONE_SLICES=$(grep '| done' "$STATE_FILE" | wc -l | xargs)
49
+
50
+ # Write status to bridge file for other hooks
51
+ BRIDGE_FILE="/tmp/gsd-cc-status-$(echo "$CWD" | md5sum 2>/dev/null | cut -c1-8 || echo "default").json"
52
+ jq -n \
53
+ --arg phase "$PHASE" \
54
+ --arg position "$POSITION" \
55
+ --arg rigor "$RIGOR" \
56
+ --arg total "$TOTAL_SLICES" \
57
+ --arg done "$DONE_SLICES" \
58
+ '{phase: $phase, position: $position, rigor: $rigor, total_slices: ($total|tonumber), done_slices: ($done|tonumber)}' \
59
+ > "$BRIDGE_FILE" 2>/dev/null
60
+
61
+ # Output statusline data
62
+ jq -n \
63
+ --arg pos "$POSITION" \
64
+ --arg phase "$PHASE_DISPLAY" \
65
+ --arg rigor "$RIGOR" \
66
+ --arg progress "${DONE_SLICES}/${TOTAL_SLICES}" \
67
+ '{
68
+ "hookSpecificOutput": {
69
+ "hookEventName": "Notification",
70
+ "additionalContext": ("GSD-CC: " + $pos + " | " + $phase + " | " + $rigor + " | " + $progress + " slices")
71
+ }
72
+ }'
73
+ exit 0
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # GSD-CC Workflow Guard — PostToolUse hook
3
+ # Nudges Claude back into the GSD-CC flow when it drifts.
4
+ # Advisory only — does not block operations.
5
+
6
+ INPUT=$(cat)
7
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
8
+ CWD=$(echo "$INPUT" | jq -r '.cwd')
9
+
10
+ # Only check Edit and Write on source files (not .gsd/ files)
11
+ if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
12
+ exit 0
13
+ fi
14
+
15
+ # Skip if not a GSD-CC project
16
+ STATE_FILE="$CWD/.gsd/STATE.md"
17
+ if [ ! -f "$STATE_FILE" ]; then
18
+ exit 0
19
+ fi
20
+
21
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
22
+ if [ -z "$FILE_PATH" ]; then
23
+ exit 0
24
+ fi
25
+
26
+ # Allow writes to .gsd/ directory (that's the workflow itself)
27
+ if [[ "$FILE_PATH" == *".gsd/"* ]] || [[ "$FILE_PATH" == *".claude/"* ]]; then
28
+ exit 0
29
+ fi
30
+
31
+ # Check if we're in an active execution phase
32
+ PHASE=$(grep '^phase:' "$STATE_FILE" | head -1 | sed 's/phase: *//')
33
+
34
+ case "$PHASE" in
35
+ "seed"|"seed-complete"|"stack-complete"|"roadmap-complete"|"plan-complete"|"discuss-complete")
36
+ # Not in execution — source file edits are unexpected
37
+ jq -n --arg phase "$PHASE" --arg file "$FILE_PATH" '{
38
+ "hookSpecificOutput": {
39
+ "hookEventName": "PostToolUse",
40
+ "additionalContext": ("Note: You edited " + $file + " but the current GSD-CC phase is \"" + $phase + "\" which is a planning phase, not execution. Source file changes should happen during the apply phase. If this was intentional, carry on. If not, consider running /gsd-cc to check the current state.")
41
+ }
42
+ }'
43
+ exit 0
44
+ ;;
45
+ "applying")
46
+ # In execution — this is expected
47
+ exit 0
48
+ ;;
49
+ *)
50
+ exit 0
51
+ ;;
52
+ esac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-cc",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Get Shit Done on Claude Code — structured AI development with your Max plan",
5
5
  "author": "Philipp Briese (https://github.com/0ui-labs)",
6
6
  "homepage": "https://github.com/0ui-labs/GSD-CC#readme",