agileflow 2.79.0 → 2.81.0

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.
@@ -20,87 +20,87 @@
20
20
  bashToolPatterns:
21
21
  # Recursive/force deletion
22
22
  - pattern: '\brm\s+-[rRf]'
23
- reason: "rm with recursive or force flags can destroy entire directories"
23
+ reason: 'rm with recursive or force flags can destroy entire directories'
24
24
 
25
25
  - pattern: '\brm\s+.*--no-preserve-root'
26
- reason: "rm with --no-preserve-root is catastrophically dangerous"
26
+ reason: 'rm with --no-preserve-root is catastrophically dangerous'
27
27
 
28
28
  - pattern: '\brm\s+-rf\s+/'
29
- reason: "rm -rf on root directory would destroy the entire system"
29
+ reason: 'rm -rf on root directory would destroy the entire system'
30
30
 
31
31
  # SQL destructive commands without WHERE clause
32
32
  - pattern: 'DELETE\s+FROM\s+\w+\s*;'
33
- reason: "DELETE without WHERE clause would delete all records"
33
+ reason: 'DELETE without WHERE clause would delete all records'
34
34
 
35
35
  - pattern: 'TRUNCATE\s+(TABLE\s+)?\w+'
36
- reason: "TRUNCATE removes all data from table"
36
+ reason: 'TRUNCATE removes all data from table'
37
37
 
38
38
  - pattern: 'DROP\s+(TABLE|DATABASE|SCHEMA|INDEX)'
39
- reason: "DROP commands permanently destroy database objects"
39
+ reason: 'DROP commands permanently destroy database objects'
40
40
 
41
41
  # Git force operations
42
42
  - pattern: 'git\s+push\s+.*--force'
43
- reason: "Force push can overwrite remote history"
43
+ reason: 'Force push can overwrite remote history'
44
44
  ask: true
45
45
 
46
46
  - pattern: 'git\s+push\s+.*-f\b'
47
- reason: "Force push can overwrite remote history"
47
+ reason: 'Force push can overwrite remote history'
48
48
  ask: true
49
49
 
50
50
  - pattern: 'git\s+reset\s+--hard'
51
- reason: "Hard reset discards uncommitted changes"
51
+ reason: 'Hard reset discards uncommitted changes'
52
52
  ask: true
53
53
 
54
54
  # Format/wipe operations
55
55
  - pattern: '\bmkfs\b'
56
- reason: "mkfs formats filesystems, destroying all data"
56
+ reason: 'mkfs formats filesystems, destroying all data'
57
57
 
58
58
  - pattern: '\bdd\s+.*of=/dev/'
59
- reason: "dd writing to device can destroy disk data"
59
+ reason: 'dd writing to device can destroy disk data'
60
60
 
61
61
  - pattern: '\bshred\b'
62
- reason: "shred permanently destroys file data"
62
+ reason: 'shred permanently destroys file data'
63
63
 
64
64
  # Credential/secret exposure
65
65
  - pattern: 'cat\s+.*\.env'
66
- reason: "Displaying .env may expose secrets"
66
+ reason: 'Displaying .env may expose secrets'
67
67
  ask: true
68
68
 
69
69
  - pattern: 'cat\s+.*/\.ssh/'
70
- reason: "Displaying SSH keys is a security risk"
70
+ reason: 'Displaying SSH keys is a security risk'
71
71
 
72
72
  - pattern: 'cat\s+.*/credentials'
73
- reason: "Displaying credentials files is a security risk"
73
+ reason: 'Displaying credentials files is a security risk'
74
74
 
75
75
  # Cloud CLI destructive operations
76
76
  - pattern: 'aws\s+s3\s+rm\s+--recursive'
77
- reason: "Recursive S3 delete can destroy entire buckets"
77
+ reason: 'Recursive S3 delete can destroy entire buckets'
78
78
  ask: true
79
79
 
80
80
  - pattern: 'aws\s+ec2\s+terminate-instances'
81
- reason: "Terminating EC2 instances is irreversible"
81
+ reason: 'Terminating EC2 instances is irreversible'
82
82
  ask: true
83
83
 
84
84
  - pattern: 'gcloud\s+.*delete'
85
- reason: "GCloud delete operations may be destructive"
85
+ reason: 'GCloud delete operations may be destructive'
86
86
  ask: true
87
87
 
88
88
  # Docker cleanup commands
89
89
  - pattern: 'docker\s+system\s+prune\s+-a'
90
- reason: "Docker prune -a removes all unused images"
90
+ reason: 'Docker prune -a removes all unused images'
91
91
  ask: true
92
92
 
93
93
  - pattern: 'docker\s+volume\s+rm'
94
- reason: "Docker volume removal may delete persistent data"
94
+ reason: 'Docker volume removal may delete persistent data'
95
95
  ask: true
96
96
 
97
97
  # npm/package manager dangerous commands
98
98
  - pattern: 'npm\s+unpublish'
99
- reason: "npm unpublish can break dependent packages"
99
+ reason: 'npm unpublish can break dependent packages'
100
100
  ask: true
101
101
 
102
102
  - pattern: 'npm\s+deprecate'
103
- reason: "npm deprecate affects package visibility"
103
+ reason: 'npm deprecate affects package visibility'
104
104
  ask: true
105
105
 
106
106
  # ============================================================================
@@ -110,19 +110,19 @@ bashToolPatterns:
110
110
 
111
111
  askPatterns:
112
112
  - pattern: 'DELETE\s+FROM\s+\w+\s+WHERE'
113
- reason: "Deleting specific records - confirm data is correct"
113
+ reason: 'Deleting specific records - confirm data is correct'
114
114
 
115
115
  - pattern: 'UPDATE\s+\w+\s+SET'
116
- reason: "Updating records - confirm scope is correct"
116
+ reason: 'Updating records - confirm scope is correct'
117
117
 
118
118
  - pattern: 'npm\s+publish'
119
- reason: "Publishing to npm is permanent"
119
+ reason: 'Publishing to npm is permanent'
120
120
 
121
121
  - pattern: 'git\s+tag\s+-d'
122
- reason: "Deleting git tags"
122
+ reason: 'Deleting git tags'
123
123
 
124
124
  - pattern: 'kubectl\s+delete'
125
- reason: "Kubernetes delete operations"
125
+ reason: 'Kubernetes delete operations'
126
126
 
127
127
  # ============================================================================
128
128
  # PATH PROTECTION
@@ -193,17 +193,17 @@ noDeletePaths:
193
193
  agileflowPatterns:
194
194
  # Protect AgileFlow infrastructure
195
195
  - pattern: 'rm.*\.agileflow'
196
- reason: "Deleting .agileflow would break AgileFlow installation"
196
+ reason: 'Deleting .agileflow would break AgileFlow installation'
197
197
 
198
198
  - pattern: 'rm.*\.claude'
199
- reason: "Deleting .claude would break Claude Code configuration"
199
+ reason: 'Deleting .claude would break Claude Code configuration'
200
200
 
201
201
  - pattern: 'rm.*status\.json'
202
- reason: "Deleting status.json would lose story tracking data"
202
+ reason: 'Deleting status.json would lose story tracking data'
203
203
 
204
204
  # Dangerous npm operations in AgileFlow context
205
205
  - pattern: 'npm\s+uninstall\s+agileflow'
206
- reason: "Uninstalling AgileFlow - confirm this is intentional"
206
+ reason: 'Uninstalling AgileFlow - confirm this is intentional'
207
207
  ask: true
208
208
 
209
209
  # ============================================================================
@@ -224,4 +224,4 @@ config:
224
224
 
225
225
  # Enable/disable prompt hooks (AI-based evaluation)
226
226
  promptHooksEnabled: false
227
- promptHookMessage: "Evaluate if this command could cause irreversible damage. Block if dangerous."
227
+ promptHookMessage: 'Evaluate if this command could cause irreversible damage. Block if dangerous.'
@@ -114,27 +114,9 @@ function parsePathRules(content) {
114
114
  */
115
115
  function getDefaultPathRules() {
116
116
  return {
117
- zeroAccessPaths: [
118
- '~/.ssh/',
119
- '~/.aws/credentials',
120
- '.env',
121
- '.env.local',
122
- '.env.production',
123
- ],
124
- readOnlyPaths: [
125
- '/etc/',
126
- '~/.bashrc',
127
- '~/.zshrc',
128
- 'package-lock.json',
129
- 'yarn.lock',
130
- '.git/',
131
- ],
132
- noDeletePaths: [
133
- '.agileflow/',
134
- '.claude/',
135
- 'docs/09-agents/status.json',
136
- 'CLAUDE.md',
137
- ],
117
+ zeroAccessPaths: ['~/.ssh/', '~/.aws/credentials', '.env', '.env.local', '.env.production'],
118
+ readOnlyPaths: ['/etc/', '~/.bashrc', '~/.zshrc', 'package-lock.json', 'yarn.lock', '.git/'],
119
+ noDeletePaths: ['.agileflow/', '.claude/', 'docs/09-agents/status.json', 'CLAUDE.md'],
138
120
  };
139
121
  }
140
122
 
@@ -247,9 +229,7 @@ function main() {
247
229
  }
248
230
 
249
231
  // Resolve to absolute path
250
- const absolutePath = path.isAbsolute(filePath)
251
- ? filePath
252
- : path.join(projectDir, filePath);
232
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
253
233
 
254
234
  // Load rules
255
235
  const rules = loadPathRules(projectDir);
@@ -15,30 +15,13 @@
15
15
  * Usage: Configured as PreToolUse hook in .claude/settings.json
16
16
  */
17
17
 
18
- const fs = require('fs');
19
- const path = require('path');
20
-
21
- // Color codes for output
22
- const c = {
23
- red: '\x1b[38;5;203m',
24
- yellow: '\x1b[38;5;215m',
25
- reset: '\x1b[0m',
26
- dim: '\x1b[2m'
27
- };
28
-
29
- /**
30
- * Find project root by looking for .agileflow directory
31
- */
32
- function findProjectRoot() {
33
- let dir = process.cwd();
34
- while (dir !== '/') {
35
- if (fs.existsSync(path.join(dir, '.agileflow'))) {
36
- return dir;
37
- }
38
- dir = path.dirname(dir);
39
- }
40
- return process.cwd();
41
- }
18
+ const {
19
+ findProjectRoot,
20
+ loadPatterns,
21
+ outputBlocked,
22
+ runDamageControlHook,
23
+ c,
24
+ } = require('./lib/damage-control-utils');
42
25
 
43
26
  /**
44
27
  * Parse simplified YAML for damage control patterns
@@ -48,7 +31,7 @@ function parseSimpleYAML(content) {
48
31
  const config = {
49
32
  bashToolPatterns: [],
50
33
  askPatterns: [],
51
- agileflowProtections: []
34
+ agileflowProtections: [],
52
35
  };
53
36
 
54
37
  let currentSection = null;
@@ -76,44 +59,28 @@ function parseSimpleYAML(content) {
76
59
  currentPattern = null;
77
60
  } else if (trimmed.startsWith('- pattern:') && currentSection) {
78
61
  // New pattern entry
79
- const patternValue = trimmed.replace('- pattern:', '').trim().replace(/^["']|["']$/g, '');
62
+ const patternValue = trimmed
63
+ .replace('- pattern:', '')
64
+ .trim()
65
+ .replace(/^["']|["']$/g, '');
80
66
  currentPattern = { pattern: patternValue };
81
67
  config[currentSection].push(currentPattern);
82
68
  } else if (trimmed.startsWith('reason:') && currentPattern) {
83
- currentPattern.reason = trimmed.replace('reason:', '').trim().replace(/^["']|["']$/g, '');
69
+ currentPattern.reason = trimmed
70
+ .replace('reason:', '')
71
+ .trim()
72
+ .replace(/^["']|["']$/g, '');
84
73
  } else if (trimmed.startsWith('flags:') && currentPattern) {
85
- currentPattern.flags = trimmed.replace('flags:', '').trim().replace(/^["']|["']$/g, '');
74
+ currentPattern.flags = trimmed
75
+ .replace('flags:', '')
76
+ .trim()
77
+ .replace(/^["']|["']$/g, '');
86
78
  }
87
79
  }
88
80
 
89
81
  return config;
90
82
  }
91
83
 
92
- /**
93
- * Load patterns configuration from YAML file
94
- */
95
- function loadPatterns(projectRoot) {
96
- const configPaths = [
97
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
98
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
99
- path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
100
- ];
101
-
102
- for (const configPath of configPaths) {
103
- if (fs.existsSync(configPath)) {
104
- try {
105
- const content = fs.readFileSync(configPath, 'utf8');
106
- return parseSimpleYAML(content);
107
- } catch (e) {
108
- // Continue to next path
109
- }
110
- }
111
- }
112
-
113
- // Return empty config if no file found (fail-open)
114
- return { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
115
- }
116
-
117
84
  /**
118
85
  * Test command against a single pattern rule
119
86
  */
@@ -135,14 +102,14 @@ function validateCommand(command, config) {
135
102
  // Check blocked patterns (bashToolPatterns + agileflowProtections)
136
103
  const blockedPatterns = [
137
104
  ...(config.bashToolPatterns || []),
138
- ...(config.agileflowProtections || [])
105
+ ...(config.agileflowProtections || []),
139
106
  ];
140
107
 
141
108
  for (const rule of blockedPatterns) {
142
109
  if (matchesPattern(command, rule)) {
143
110
  return {
144
111
  action: 'block',
145
- reason: rule.reason || 'Command blocked by damage control'
112
+ reason: rule.reason || 'Command blocked by damage control',
146
113
  };
147
114
  }
148
115
  }
@@ -152,7 +119,7 @@ function validateCommand(command, config) {
152
119
  if (matchesPattern(command, rule)) {
153
120
  return {
154
121
  action: 'ask',
155
- reason: rule.reason || 'Please confirm this command'
122
+ reason: rule.reason || 'Please confirm this command',
156
123
  };
157
124
  }
158
125
  }
@@ -161,72 +128,18 @@ function validateCommand(command, config) {
161
128
  return { action: 'allow' };
162
129
  }
163
130
 
164
- /**
165
- * Main function - read input and validate
166
- */
167
- function main() {
168
- const projectRoot = findProjectRoot();
169
- let inputData = '';
170
-
171
- process.stdin.setEncoding('utf8');
172
-
173
- process.stdin.on('data', chunk => {
174
- inputData += chunk;
175
- });
176
-
177
- process.stdin.on('end', () => {
178
- try {
179
- // Parse tool input from Claude Code
180
- const input = JSON.parse(inputData);
181
- const command = input.command || input.tool_input?.command || '';
182
-
183
- if (!command) {
184
- // No command to validate - allow
185
- process.exit(0);
186
- }
187
-
188
- // Load patterns and validate
189
- const config = loadPatterns(projectRoot);
190
- const result = validateCommand(command, config);
191
-
192
- switch (result.action) {
193
- case 'block':
194
- // Output error message and block
195
- console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
196
- console.error(`${c.dim}Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}${c.reset}`);
197
- process.exit(2);
198
- break;
199
-
200
- case 'ask':
201
- // Output JSON to trigger user confirmation
202
- console.log(JSON.stringify({
203
- result: 'ask',
204
- message: result.reason
205
- }));
206
- process.exit(0);
207
- break;
208
-
209
- case 'allow':
210
- default:
211
- // Allow command to proceed
212
- process.exit(0);
213
- }
214
- } catch (e) {
215
- // Parse error or other issue - fail open
216
- // This ensures broken config doesn't block all commands
217
- process.exit(0);
218
- }
219
- });
220
-
221
- // Handle no stdin (direct invocation)
222
- process.stdin.on('error', () => {
223
- process.exit(0);
224
- });
225
-
226
- // Set timeout to prevent hanging
227
- setTimeout(() => {
228
- process.exit(0);
229
- }, 4000);
230
- }
231
-
232
- main();
131
+ // Run the hook
132
+ const projectRoot = findProjectRoot();
133
+ const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
134
+
135
+ runDamageControlHook({
136
+ getInputValue: input => input.command || input.tool_input?.command,
137
+ loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
138
+ validate: validateCommand,
139
+ onBlock: (result, command) => {
140
+ outputBlocked(
141
+ result.reason,
142
+ `Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`
143
+ );
144
+ },
145
+ });
@@ -12,40 +12,13 @@
12
12
  * Usage: Configured as PreToolUse hook in .claude/settings.json
13
13
  */
14
14
 
15
- const fs = require('fs');
16
- const path = require('path');
17
- const os = require('os');
18
-
19
- // Color codes for output
20
- const c = {
21
- red: '\x1b[38;5;203m',
22
- reset: '\x1b[0m',
23
- dim: '\x1b[2m'
24
- };
25
-
26
- /**
27
- * Find project root by looking for .agileflow directory
28
- */
29
- function findProjectRoot() {
30
- let dir = process.cwd();
31
- while (dir !== '/') {
32
- if (fs.existsSync(path.join(dir, '.agileflow'))) {
33
- return dir;
34
- }
35
- dir = path.dirname(dir);
36
- }
37
- return process.cwd();
38
- }
39
-
40
- /**
41
- * Expand ~ to home directory
42
- */
43
- function expandPath(p) {
44
- if (p.startsWith('~/')) {
45
- return path.join(os.homedir(), p.slice(2));
46
- }
47
- return p;
48
- }
15
+ const {
16
+ findProjectRoot,
17
+ loadPatterns,
18
+ pathMatches,
19
+ outputBlocked,
20
+ runDamageControlHook,
21
+ } = require('./lib/damage-control-utils');
49
22
 
50
23
  /**
51
24
  * Parse simplified YAML for path patterns
@@ -54,7 +27,7 @@ function parseSimpleYAML(content) {
54
27
  const config = {
55
28
  zeroAccessPaths: [],
56
29
  readOnlyPaths: [],
57
- noDeletePaths: []
30
+ noDeletePaths: [],
58
31
  };
59
32
 
60
33
  let currentSection = null;
@@ -85,79 +58,6 @@ function parseSimpleYAML(content) {
85
58
  return config;
86
59
  }
87
60
 
88
- /**
89
- * Load patterns configuration from YAML file
90
- */
91
- function loadPatterns(projectRoot) {
92
- const configPaths = [
93
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
94
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
95
- path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
96
- ];
97
-
98
- for (const configPath of configPaths) {
99
- if (fs.existsSync(configPath)) {
100
- try {
101
- const content = fs.readFileSync(configPath, 'utf8');
102
- return parseSimpleYAML(content);
103
- } catch (e) {
104
- // Continue to next path
105
- }
106
- }
107
- }
108
-
109
- // Return empty config if no file found (fail-open)
110
- return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
111
- }
112
-
113
- /**
114
- * Check if a file path matches any of the protected patterns
115
- */
116
- function pathMatches(filePath, patterns) {
117
- if (!filePath) return null;
118
-
119
- const normalizedPath = path.resolve(filePath);
120
- const relativePath = path.relative(process.cwd(), normalizedPath);
121
-
122
- for (const pattern of patterns) {
123
- const expandedPattern = expandPath(pattern);
124
-
125
- // Check if pattern is a directory prefix
126
- if (pattern.endsWith('/')) {
127
- const patternDir = expandedPattern.slice(0, -1);
128
- if (normalizedPath.startsWith(patternDir)) {
129
- return pattern;
130
- }
131
- }
132
-
133
- // Check exact match
134
- if (normalizedPath === expandedPattern) {
135
- return pattern;
136
- }
137
-
138
- // Check if normalized path ends with pattern (for filenames like "id_rsa")
139
- if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
140
- return pattern;
141
- }
142
-
143
- // Check if pattern appears in path (for patterns like "*.pem")
144
- if (pattern.startsWith('*')) {
145
- const ext = pattern.slice(1);
146
- if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
147
- return pattern;
148
- }
149
- }
150
-
151
- // Check if path contains pattern (for things like ".env.production")
152
- const patternBase = path.basename(pattern);
153
- if (path.basename(normalizedPath) === patternBase) {
154
- return pattern;
155
- }
156
- }
157
-
158
- return null;
159
- }
160
-
161
61
  /**
162
62
  * Validate file path for edit operation
163
63
  */
@@ -168,7 +68,7 @@ function validatePath(filePath, config) {
168
68
  return {
169
69
  action: 'block',
170
70
  reason: `Zero-access path: ${zeroMatch}`,
171
- detail: 'This file is protected and cannot be accessed'
71
+ detail: 'This file is protected and cannot be accessed',
172
72
  };
173
73
  }
174
74
 
@@ -178,7 +78,7 @@ function validatePath(filePath, config) {
178
78
  return {
179
79
  action: 'block',
180
80
  reason: `Read-only path: ${readOnlyMatch}`,
181
- detail: 'This file is read-only and cannot be edited'
81
+ detail: 'This file is read-only and cannot be edited',
182
82
  };
183
83
  }
184
84
 
@@ -186,58 +86,15 @@ function validatePath(filePath, config) {
186
86
  return { action: 'allow' };
187
87
  }
188
88
 
189
- /**
190
- * Main function - read input and validate
191
- */
192
- function main() {
193
- const projectRoot = findProjectRoot();
194
- let inputData = '';
195
-
196
- process.stdin.setEncoding('utf8');
197
-
198
- process.stdin.on('data', chunk => {
199
- inputData += chunk;
200
- });
201
-
202
- process.stdin.on('end', () => {
203
- try {
204
- // Parse tool input from Claude Code
205
- const input = JSON.parse(inputData);
206
- const filePath = input.file_path || input.tool_input?.file_path || '';
207
-
208
- if (!filePath) {
209
- // No path to validate - allow
210
- process.exit(0);
211
- }
212
-
213
- // Load patterns and validate
214
- const config = loadPatterns(projectRoot);
215
- const result = validatePath(filePath, config);
216
-
217
- if (result.action === 'block') {
218
- console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
219
- console.error(`${c.dim}${result.detail}${c.reset}`);
220
- console.error(`${c.dim}File: ${filePath}${c.reset}`);
221
- process.exit(2);
222
- }
223
-
224
- // Allow
225
- process.exit(0);
226
- } catch (e) {
227
- // Parse error or other issue - fail open
228
- process.exit(0);
229
- }
230
- });
231
-
232
- // Handle no stdin
233
- process.stdin.on('error', () => {
234
- process.exit(0);
235
- });
236
-
237
- // Set timeout to prevent hanging
238
- setTimeout(() => {
239
- process.exit(0);
240
- }, 4000);
241
- }
242
-
243
- main();
89
+ // Run the hook
90
+ const projectRoot = findProjectRoot();
91
+ const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
92
+
93
+ runDamageControlHook({
94
+ getInputValue: input => input.file_path || input.tool_input?.file_path,
95
+ loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
96
+ validate: validatePath,
97
+ onBlock: (result, filePath) => {
98
+ outputBlocked(result.reason, result.detail, `File: ${filePath}`);
99
+ },
100
+ });