jettypod 4.4.54 → 4.4.55

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.
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Global Guardrails Hook
4
+ *
5
+ * Enforces actions that are ALWAYS allowed and ALWAYS blocked regardless of
6
+ * skill or context. Runs before any contextual/skill-specific validators.
7
+ *
8
+ * Claude Code PreToolUse Hook
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ // Read hook input from stdin
15
+ let input = '';
16
+ process.stdin.on('data', chunk => input += chunk);
17
+ process.stdin.on('end', () => {
18
+ try {
19
+ const hookInput = JSON.parse(input);
20
+ const result = evaluateRequest(hookInput);
21
+
22
+ if (result.allowed) {
23
+ allow();
24
+ } else {
25
+ deny(result.message, result.hint);
26
+ }
27
+ } catch (err) {
28
+ // On parse error, allow (fail open)
29
+ console.error('Hook error:', err.message);
30
+ allow();
31
+ }
32
+ });
33
+
34
+ /**
35
+ * Evaluate the request and return allow/deny decision
36
+ */
37
+ function evaluateRequest(hookInput) {
38
+ const { tool_name, tool_input, cwd, active_worktree_path, current_branch } = hookInput;
39
+
40
+ // ALWAYS ALLOW: Read operations
41
+ if (tool_name === 'Read' || tool_name === 'Glob' || tool_name === 'Grep') {
42
+ return { allowed: true };
43
+ }
44
+
45
+ // Handle Bash commands
46
+ if (tool_name === 'Bash') {
47
+ return evaluateBashCommand(tool_input.command || '', current_branch, cwd);
48
+ }
49
+
50
+ // Handle Write/Edit operations
51
+ if (tool_name === 'Write' || tool_name === 'Edit') {
52
+ return evaluateWriteOperation(tool_input.file_path || '', active_worktree_path, cwd);
53
+ }
54
+
55
+ // Default: allow unknown tools
56
+ return { allowed: true };
57
+ }
58
+
59
+ /**
60
+ * Evaluate Bash command against global rules
61
+ */
62
+ function evaluateBashCommand(command, currentBranch, cwd) {
63
+ // BLOCKED: Force push
64
+ if (/git\s+push\s+.*--force/.test(command) || /git\s+push\s+-f\b/.test(command)) {
65
+ return {
66
+ allowed: false,
67
+ message: 'Force push is blocked',
68
+ hint: 'Force pushing can destroy history. Use regular push or create a PR.'
69
+ };
70
+ }
71
+
72
+ // BLOCKED: Direct commit to main
73
+ if (/git\s+commit\b/.test(command) && currentBranch === 'main') {
74
+ return {
75
+ allowed: false,
76
+ message: 'Direct commits to main are blocked',
77
+ hint: 'Use jettypod work start to create a feature branch first.'
78
+ };
79
+ }
80
+
81
+ // BLOCKED: Manual worktree creation
82
+ if (/git\s+worktree\s+add\b/.test(command)) {
83
+ return {
84
+ allowed: false,
85
+ message: 'Manual worktree creation is blocked',
86
+ hint: 'Use jettypod work start to create worktrees.'
87
+ };
88
+ }
89
+
90
+ // BLOCKED: Manual branch creation
91
+ if (/git\s+checkout\s+-b\b/.test(command) || /git\s+branch\s+(?!-d|-D)/.test(command)) {
92
+ return {
93
+ allowed: false,
94
+ message: 'Manual branch creation is blocked',
95
+ hint: 'Use jettypod work start to create branches.'
96
+ };
97
+ }
98
+
99
+ // BLOCKED: Direct SQL mutation to work.db
100
+ if (/sqlite3\s+.*work\.db/.test(command)) {
101
+ const sqlCommand = command.toLowerCase();
102
+ // Allow SELECT, block mutations
103
+ if (/\b(update|insert|delete|drop|alter|create)\b/i.test(sqlCommand)) {
104
+ return {
105
+ allowed: false,
106
+ message: 'Direct database mutations are blocked',
107
+ hint: 'Use jettypod CLI commands to modify work items.'
108
+ };
109
+ }
110
+ }
111
+
112
+ // BLOCKED: Merge from inside worktree (causes CWD corruption)
113
+ if (/jettypod\s+(work|tests)\s+merge/.test(command)) {
114
+ if (cwd && /\.jettypod-work\//.test(cwd)) {
115
+ return {
116
+ allowed: false,
117
+ message: 'Cannot merge from inside a worktree. CWD would be deleted.',
118
+ hint: 'Run: cd /Users/erikspangenberg/jettypod-source && jettypod work merge <id>'
119
+ };
120
+ }
121
+ }
122
+
123
+ // ALLOWED: Git read-only commands
124
+ if (/git\s+(status|log|diff|show|branch\s*$|remote|fetch)\b/.test(command)) {
125
+ return { allowed: true };
126
+ }
127
+
128
+ // ALLOWED: Jettypod read-only commands
129
+ if (/jettypod\s+(backlog|status|work\s+status|impact)\b/.test(command)) {
130
+ return { allowed: true };
131
+ }
132
+
133
+ // Default: allow other commands
134
+ return { allowed: true };
135
+ }
136
+
137
+ /**
138
+ * Find the jettypod database path
139
+ * @param {string} cwd - Starting directory
140
+ * @returns {string|null} Database path or null
141
+ */
142
+ function findDatabasePath(cwd) {
143
+ let dir = cwd;
144
+ while (dir !== path.dirname(dir)) {
145
+ const dbPath = path.join(dir, '.jettypod', 'work.db');
146
+ if (fs.existsSync(dbPath)) {
147
+ return dbPath;
148
+ }
149
+ dir = path.dirname(dir);
150
+ }
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * Get the active worktree path from database
156
+ * @param {string} cwd - Current working directory
157
+ * @returns {string|null} Active worktree path or null
158
+ */
159
+ function getActiveWorktreePathFromDB(cwd) {
160
+ const dbPath = findDatabasePath(cwd);
161
+ if (!dbPath) {
162
+ return null;
163
+ }
164
+
165
+ try {
166
+ // Try better-sqlite3 first
167
+ const sqlite3 = require('better-sqlite3');
168
+ const db = sqlite3(dbPath, { readonly: true });
169
+ const row = db.prepare(
170
+ `SELECT worktree_path FROM worktrees WHERE status = 'active' LIMIT 1`
171
+ ).get();
172
+ db.close();
173
+ return row ? row.worktree_path : null;
174
+ } catch (err) {
175
+ // Fall back to CLI
176
+ const { spawnSync } = require('child_process');
177
+ const result = spawnSync('sqlite3', [
178
+ dbPath,
179
+ `SELECT worktree_path FROM worktrees WHERE status = 'active' LIMIT 1`
180
+ ], { encoding: 'utf-8' });
181
+
182
+ if (result.error || result.status !== 0) {
183
+ return null;
184
+ }
185
+ return result.stdout.trim() || null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Evaluate Write/Edit operation against global rules
191
+ */
192
+ function evaluateWriteOperation(filePath, inputWorktreePath, cwd) {
193
+ // Query database for active worktree, fall back to input if provided
194
+ const activeWorktreePath = getActiveWorktreePathFromDB(cwd) || inputWorktreePath;
195
+
196
+ // Normalize paths
197
+ const normalizedPath = path.resolve(cwd || '.', filePath);
198
+ const normalizedWorktree = activeWorktreePath ? path.resolve(cwd || '.', activeWorktreePath) : null;
199
+
200
+ // BLOCKED: Protected files (skills, hooks)
201
+ const protectedPatterns = [
202
+ /\.claude\/skills\//i,
203
+ /claude-hooks\//i,
204
+ /\.jettypod\/hooks\//i
205
+ ];
206
+
207
+ for (const pattern of protectedPatterns) {
208
+ if (pattern.test(normalizedPath) || pattern.test(filePath)) {
209
+ return {
210
+ allowed: false,
211
+ message: 'Protected file - cannot modify',
212
+ hint: 'Skill and hook files are protected. Modify them through proper channels.'
213
+ };
214
+ }
215
+ }
216
+
217
+ // Check if path is in a worktree
218
+ const isInWorktree = /\.jettypod-work\//.test(filePath) || /\.jettypod-work\//.test(normalizedPath);
219
+
220
+ if (isInWorktree) {
221
+ // If we have an active worktree, check if this write is to it
222
+ if (activeWorktreePath) {
223
+ // SECURITY: Only use normalized path comparison to prevent traversal attacks
224
+ // The filePath.includes() check was vulnerable to "../" escapes
225
+ const isActiveWorktree = normalizedPath.startsWith(normalizedWorktree + path.sep) ||
226
+ normalizedPath === normalizedWorktree;
227
+
228
+ if (isActiveWorktree) {
229
+ return { allowed: true };
230
+ } else {
231
+ return {
232
+ allowed: false,
233
+ message: 'Not your active worktree',
234
+ hint: 'You can only write to your currently active worktree.'
235
+ };
236
+ }
237
+ }
238
+ // No active worktree tracked but path looks like worktree - block
239
+ return {
240
+ allowed: false,
241
+ message: 'Not your active worktree',
242
+ hint: 'You can only write to your currently active worktree.'
243
+ };
244
+ }
245
+
246
+ // BLOCKED: Write to main repo (not in worktree)
247
+ return {
248
+ allowed: false,
249
+ message: 'Cannot write to main repository',
250
+ hint: 'Use jettypod work start to create a worktree first.'
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Allow the action
256
+ */
257
+ function allow() {
258
+ console.log(JSON.stringify({
259
+ hookSpecificOutput: {
260
+ hookEventName: "PreToolUse",
261
+ permissionDecision: "allow"
262
+ }
263
+ }));
264
+ process.exit(0);
265
+ }
266
+
267
+ /**
268
+ * Deny the action with explanation
269
+ */
270
+ function deny(message, hint) {
271
+ const reason = `āŒ ${message}\n\nšŸ’” Hint: ${hint || 'Check your action.'}`;
272
+
273
+ console.log(JSON.stringify({
274
+ hookSpecificOutput: {
275
+ hookEventName: "PreToolUse",
276
+ permissionDecision: "deny",
277
+ permissionDecisionReason: reason
278
+ }
279
+ }));
280
+ process.exit(0);
281
+ }
package/jettypod.js CHANGED
@@ -785,6 +785,14 @@ async function initializeProject() {
785
785
  fs.chmodSync(enforceHookDest, 0o755);
786
786
  }
787
787
 
788
+ // Install global-guardrails hook
789
+ const guardrailsHookSource = path.join(__dirname, 'claude-hooks', 'global-guardrails.js');
790
+ const guardrailsHookDest = path.join('.jettypod', 'hooks', 'global-guardrails.js');
791
+ if (fs.existsSync(guardrailsHookSource)) {
792
+ fs.copyFileSync(guardrailsHookSource, guardrailsHookDest);
793
+ fs.chmodSync(guardrailsHookDest, 0o755);
794
+ }
795
+
788
796
  // Create Claude Code settings
789
797
  if (!fs.existsSync('.claude')) {
790
798
  fs.mkdirSync('.claude', { recursive: true });
@@ -800,15 +808,24 @@ async function initializeProject() {
800
808
  PreToolUse: [
801
809
  {
802
810
  matcher: 'Edit',
803
- hooks: [{ type: 'command', command: '.jettypod/hooks/protect-claude-md.js' }]
811
+ hooks: [
812
+ { type: 'command', command: '.jettypod/hooks/global-guardrails.js' },
813
+ { type: 'command', command: '.jettypod/hooks/protect-claude-md.js' }
814
+ ]
804
815
  },
805
816
  {
806
817
  matcher: 'Write',
807
- hooks: [{ type: 'command', command: '.jettypod/hooks/protect-claude-md.js' }]
818
+ hooks: [
819
+ { type: 'command', command: '.jettypod/hooks/global-guardrails.js' },
820
+ { type: 'command', command: '.jettypod/hooks/protect-claude-md.js' }
821
+ ]
808
822
  },
809
823
  {
810
824
  matcher: 'Bash',
811
- hooks: [{ type: 'command', command: '.jettypod/hooks/enforce-skill-activation.js' }]
825
+ hooks: [
826
+ { type: 'command', command: '.jettypod/hooks/global-guardrails.js' },
827
+ { type: 'command', command: '.jettypod/hooks/enforce-skill-activation.js' }
828
+ ]
812
829
  }
813
830
  ]
814
831
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.54",
3
+ "version": "4.4.55",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {