agileflow 2.78.0 → 2.80.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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +126 -17
- package/scripts/agileflow-welcome.js +77 -98
- package/scripts/auto-self-improve.js +23 -45
- package/scripts/check-update.js +35 -42
- package/scripts/damage-control/bash-tool-damage-control.js +258 -0
- package/scripts/damage-control/edit-tool-damage-control.js +259 -0
- package/scripts/damage-control/patterns.yaml +227 -0
- package/scripts/damage-control/write-tool-damage-control.js +254 -0
- package/scripts/damage-control-bash.js +28 -22
- package/scripts/damage-control-edit.js +6 -12
- package/scripts/damage-control-write.js +6 -12
- package/scripts/get-env.js +6 -6
- package/scripts/obtain-context.js +67 -37
- package/scripts/ralph-loop.js +199 -63
- package/scripts/screenshot-verifier.js +215 -0
- package/scripts/session-manager.js +12 -33
- package/src/core/agents/configuration-damage-control.md +248 -0
- package/src/core/commands/babysit.md +30 -2
- package/src/core/commands/configure.md +46 -9
- package/src/core/commands/setup/visual-e2e.md +462 -0
- package/src/core/experts/documentation/expertise.yaml +25 -0
- package/src/core/skills/_learnings/code-review.yaml +118 -0
- package/src/core/skills/_learnings/story-writer.yaml +71 -0
- package/tools/cli/commands/start.js +19 -21
- package/tools/cli/installers/ide/claude-code.js +140 -0
- package/tools/cli/tui/Dashboard.js +3 -4
- package/tools/postinstall.js +1 -9
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# AgileFlow Damage Control - Security Patterns
|
|
2
|
+
#
|
|
3
|
+
# This file defines patterns for blocking destructive commands and protecting
|
|
4
|
+
# sensitive paths. The damage control hooks read this file to determine
|
|
5
|
+
# whether to allow, block, or ask for confirmation before executing commands.
|
|
6
|
+
#
|
|
7
|
+
# Exit codes from hooks:
|
|
8
|
+
# 0 = Allow command to proceed
|
|
9
|
+
# 2 = Block command (show error to user)
|
|
10
|
+
#
|
|
11
|
+
# To ask for confirmation, hooks output JSON: {"result": "ask", "message": "..."}
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# BASH TOOL PATTERNS
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# Regex patterns to match against bash commands
|
|
17
|
+
# If matched with ask: false (or no ask key), command is BLOCKED
|
|
18
|
+
# If matched with ask: true, user is asked for confirmation
|
|
19
|
+
|
|
20
|
+
bashToolPatterns:
|
|
21
|
+
# Recursive/force deletion
|
|
22
|
+
- pattern: '\brm\s+-[rRf]'
|
|
23
|
+
reason: 'rm with recursive or force flags can destroy entire directories'
|
|
24
|
+
|
|
25
|
+
- pattern: '\brm\s+.*--no-preserve-root'
|
|
26
|
+
reason: 'rm with --no-preserve-root is catastrophically dangerous'
|
|
27
|
+
|
|
28
|
+
- pattern: '\brm\s+-rf\s+/'
|
|
29
|
+
reason: 'rm -rf on root directory would destroy the entire system'
|
|
30
|
+
|
|
31
|
+
# SQL destructive commands without WHERE clause
|
|
32
|
+
- pattern: 'DELETE\s+FROM\s+\w+\s*;'
|
|
33
|
+
reason: 'DELETE without WHERE clause would delete all records'
|
|
34
|
+
|
|
35
|
+
- pattern: 'TRUNCATE\s+(TABLE\s+)?\w+'
|
|
36
|
+
reason: 'TRUNCATE removes all data from table'
|
|
37
|
+
|
|
38
|
+
- pattern: 'DROP\s+(TABLE|DATABASE|SCHEMA|INDEX)'
|
|
39
|
+
reason: 'DROP commands permanently destroy database objects'
|
|
40
|
+
|
|
41
|
+
# Git force operations
|
|
42
|
+
- pattern: 'git\s+push\s+.*--force'
|
|
43
|
+
reason: 'Force push can overwrite remote history'
|
|
44
|
+
ask: true
|
|
45
|
+
|
|
46
|
+
- pattern: 'git\s+push\s+.*-f\b'
|
|
47
|
+
reason: 'Force push can overwrite remote history'
|
|
48
|
+
ask: true
|
|
49
|
+
|
|
50
|
+
- pattern: 'git\s+reset\s+--hard'
|
|
51
|
+
reason: 'Hard reset discards uncommitted changes'
|
|
52
|
+
ask: true
|
|
53
|
+
|
|
54
|
+
# Format/wipe operations
|
|
55
|
+
- pattern: '\bmkfs\b'
|
|
56
|
+
reason: 'mkfs formats filesystems, destroying all data'
|
|
57
|
+
|
|
58
|
+
- pattern: '\bdd\s+.*of=/dev/'
|
|
59
|
+
reason: 'dd writing to device can destroy disk data'
|
|
60
|
+
|
|
61
|
+
- pattern: '\bshred\b'
|
|
62
|
+
reason: 'shred permanently destroys file data'
|
|
63
|
+
|
|
64
|
+
# Credential/secret exposure
|
|
65
|
+
- pattern: 'cat\s+.*\.env'
|
|
66
|
+
reason: 'Displaying .env may expose secrets'
|
|
67
|
+
ask: true
|
|
68
|
+
|
|
69
|
+
- pattern: 'cat\s+.*/\.ssh/'
|
|
70
|
+
reason: 'Displaying SSH keys is a security risk'
|
|
71
|
+
|
|
72
|
+
- pattern: 'cat\s+.*/credentials'
|
|
73
|
+
reason: 'Displaying credentials files is a security risk'
|
|
74
|
+
|
|
75
|
+
# Cloud CLI destructive operations
|
|
76
|
+
- pattern: 'aws\s+s3\s+rm\s+--recursive'
|
|
77
|
+
reason: 'Recursive S3 delete can destroy entire buckets'
|
|
78
|
+
ask: true
|
|
79
|
+
|
|
80
|
+
- pattern: 'aws\s+ec2\s+terminate-instances'
|
|
81
|
+
reason: 'Terminating EC2 instances is irreversible'
|
|
82
|
+
ask: true
|
|
83
|
+
|
|
84
|
+
- pattern: 'gcloud\s+.*delete'
|
|
85
|
+
reason: 'GCloud delete operations may be destructive'
|
|
86
|
+
ask: true
|
|
87
|
+
|
|
88
|
+
# Docker cleanup commands
|
|
89
|
+
- pattern: 'docker\s+system\s+prune\s+-a'
|
|
90
|
+
reason: 'Docker prune -a removes all unused images'
|
|
91
|
+
ask: true
|
|
92
|
+
|
|
93
|
+
- pattern: 'docker\s+volume\s+rm'
|
|
94
|
+
reason: 'Docker volume removal may delete persistent data'
|
|
95
|
+
ask: true
|
|
96
|
+
|
|
97
|
+
# npm/package manager dangerous commands
|
|
98
|
+
- pattern: 'npm\s+unpublish'
|
|
99
|
+
reason: 'npm unpublish can break dependent packages'
|
|
100
|
+
ask: true
|
|
101
|
+
|
|
102
|
+
- pattern: 'npm\s+deprecate'
|
|
103
|
+
reason: 'npm deprecate affects package visibility'
|
|
104
|
+
ask: true
|
|
105
|
+
|
|
106
|
+
# ============================================================================
|
|
107
|
+
# ASK PATTERNS (CONFIRMATION REQUIRED)
|
|
108
|
+
# ============================================================================
|
|
109
|
+
# These patterns trigger a confirmation prompt but don't block by default
|
|
110
|
+
|
|
111
|
+
askPatterns:
|
|
112
|
+
- pattern: 'DELETE\s+FROM\s+\w+\s+WHERE'
|
|
113
|
+
reason: 'Deleting specific records - confirm data is correct'
|
|
114
|
+
|
|
115
|
+
- pattern: 'UPDATE\s+\w+\s+SET'
|
|
116
|
+
reason: 'Updating records - confirm scope is correct'
|
|
117
|
+
|
|
118
|
+
- pattern: 'npm\s+publish'
|
|
119
|
+
reason: 'Publishing to npm is permanent'
|
|
120
|
+
|
|
121
|
+
- pattern: 'git\s+tag\s+-d'
|
|
122
|
+
reason: 'Deleting git tags'
|
|
123
|
+
|
|
124
|
+
- pattern: 'kubectl\s+delete'
|
|
125
|
+
reason: 'Kubernetes delete operations'
|
|
126
|
+
|
|
127
|
+
# ============================================================================
|
|
128
|
+
# PATH PROTECTION
|
|
129
|
+
# ============================================================================
|
|
130
|
+
# Three protection levels:
|
|
131
|
+
# zeroAccessPaths: Cannot read, write, edit, or delete
|
|
132
|
+
# readOnlyPaths: Can read, cannot modify or delete
|
|
133
|
+
# noDeletePaths: Can read and modify, cannot delete
|
|
134
|
+
|
|
135
|
+
zeroAccessPaths:
|
|
136
|
+
# System credentials
|
|
137
|
+
- ~/.ssh/
|
|
138
|
+
- ~/.gnupg/
|
|
139
|
+
- ~/.aws/credentials
|
|
140
|
+
- ~/.config/gcloud/
|
|
141
|
+
|
|
142
|
+
# Environment secrets
|
|
143
|
+
- .env
|
|
144
|
+
- .env.local
|
|
145
|
+
- .env.production
|
|
146
|
+
- .env.*.local
|
|
147
|
+
|
|
148
|
+
# Secret files
|
|
149
|
+
- '**/secrets/**'
|
|
150
|
+
- '**/credentials/**'
|
|
151
|
+
- '**/*.pem'
|
|
152
|
+
- '**/*.key'
|
|
153
|
+
- '**/id_rsa'
|
|
154
|
+
- '**/id_ed25519'
|
|
155
|
+
|
|
156
|
+
readOnlyPaths:
|
|
157
|
+
# System config
|
|
158
|
+
- /etc/
|
|
159
|
+
- ~/.bashrc
|
|
160
|
+
- ~/.zshrc
|
|
161
|
+
- ~/.profile
|
|
162
|
+
|
|
163
|
+
# Package locks (should use npm install, not edit directly)
|
|
164
|
+
- package-lock.json
|
|
165
|
+
- yarn.lock
|
|
166
|
+
- pnpm-lock.yaml
|
|
167
|
+
|
|
168
|
+
# Git internals
|
|
169
|
+
- .git/
|
|
170
|
+
|
|
171
|
+
noDeletePaths:
|
|
172
|
+
# AgileFlow core
|
|
173
|
+
- .agileflow/
|
|
174
|
+
- .agileflow/config.json
|
|
175
|
+
- .agileflow/scripts/
|
|
176
|
+
|
|
177
|
+
# Claude Code hooks and commands
|
|
178
|
+
- .claude/
|
|
179
|
+
- .claude/hooks/
|
|
180
|
+
- .claude/commands/
|
|
181
|
+
- .claude/settings.json
|
|
182
|
+
|
|
183
|
+
# Project documentation (can edit, can't delete)
|
|
184
|
+
- docs/09-agents/status.json
|
|
185
|
+
- CLAUDE.md
|
|
186
|
+
- AGENTS.md
|
|
187
|
+
|
|
188
|
+
# ============================================================================
|
|
189
|
+
# AGILEFLOW-SPECIFIC PROTECTION
|
|
190
|
+
# ============================================================================
|
|
191
|
+
# Additional patterns specific to AgileFlow projects
|
|
192
|
+
|
|
193
|
+
agileflowPatterns:
|
|
194
|
+
# Protect AgileFlow infrastructure
|
|
195
|
+
- pattern: 'rm.*\.agileflow'
|
|
196
|
+
reason: 'Deleting .agileflow would break AgileFlow installation'
|
|
197
|
+
|
|
198
|
+
- pattern: 'rm.*\.claude'
|
|
199
|
+
reason: 'Deleting .claude would break Claude Code configuration'
|
|
200
|
+
|
|
201
|
+
- pattern: 'rm.*status\.json'
|
|
202
|
+
reason: 'Deleting status.json would lose story tracking data'
|
|
203
|
+
|
|
204
|
+
# Dangerous npm operations in AgileFlow context
|
|
205
|
+
- pattern: 'npm\s+uninstall\s+agileflow'
|
|
206
|
+
reason: 'Uninstalling AgileFlow - confirm this is intentional'
|
|
207
|
+
ask: true
|
|
208
|
+
|
|
209
|
+
# ============================================================================
|
|
210
|
+
# CONFIGURATION
|
|
211
|
+
# ============================================================================
|
|
212
|
+
# Settings for the damage control system
|
|
213
|
+
|
|
214
|
+
config:
|
|
215
|
+
# Log blocked commands for pattern refinement
|
|
216
|
+
logBlocked: true
|
|
217
|
+
logPath: .agileflow/logs/damage-control.log
|
|
218
|
+
|
|
219
|
+
# Show detailed reason when blocking
|
|
220
|
+
showBlockReason: true
|
|
221
|
+
|
|
222
|
+
# Timeout for hook execution (seconds)
|
|
223
|
+
hookTimeout: 5
|
|
224
|
+
|
|
225
|
+
# Enable/disable prompt hooks (AI-based evaluation)
|
|
226
|
+
promptHooksEnabled: false
|
|
227
|
+
promptHookMessage: 'Evaluate if this command could cause irreversible damage. Block if dangerous.'
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* write-tool-damage-control.js - Enforce path protection for Write tool
|
|
5
|
+
*
|
|
6
|
+
* This PreToolUse hook runs before every Write tool execution.
|
|
7
|
+
* It checks the file path against patterns.yaml to block writes
|
|
8
|
+
* to protected paths.
|
|
9
|
+
*
|
|
10
|
+
* Path protection levels:
|
|
11
|
+
* zeroAccessPaths: Cannot read, write, edit, or delete
|
|
12
|
+
* readOnlyPaths: Can read, cannot write or delete
|
|
13
|
+
* noDeletePaths: Can read and write, cannot delete (Write is allowed)
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* 0 = Allow write to proceed
|
|
17
|
+
* 2 = Block write
|
|
18
|
+
*
|
|
19
|
+
* Usage (as PreToolUse hook):
|
|
20
|
+
* node .claude/hooks/damage-control/write-tool-damage-control.js
|
|
21
|
+
*
|
|
22
|
+
* Environment:
|
|
23
|
+
* CLAUDE_TOOL_INPUT - JSON string with tool input (contains "file_path")
|
|
24
|
+
* CLAUDE_PROJECT_DIR - Project root directory
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
// ANSI colors for output
|
|
31
|
+
const c = {
|
|
32
|
+
reset: '\x1b[0m',
|
|
33
|
+
bold: '\x1b[1m',
|
|
34
|
+
red: '\x1b[31m',
|
|
35
|
+
yellow: '\x1b[33m',
|
|
36
|
+
cyan: '\x1b[36m',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Exit codes
|
|
40
|
+
const EXIT_ALLOW = 0;
|
|
41
|
+
const EXIT_BLOCK = 2;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load path protection rules from patterns.yaml
|
|
45
|
+
*/
|
|
46
|
+
function loadPathRules(projectDir) {
|
|
47
|
+
const locations = [
|
|
48
|
+
path.join(projectDir, '.claude/hooks/damage-control/patterns.yaml'),
|
|
49
|
+
path.join(projectDir, '.agileflow/hooks/damage-control/patterns.yaml'),
|
|
50
|
+
path.join(projectDir, 'patterns.yaml'),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const loc of locations) {
|
|
54
|
+
if (fs.existsSync(loc)) {
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(loc, 'utf8');
|
|
57
|
+
return parsePathRules(content);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`Warning: Could not parse ${loc}: ${e.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return getDefaultPathRules();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse path rules from YAML content
|
|
69
|
+
*/
|
|
70
|
+
function parsePathRules(content) {
|
|
71
|
+
const rules = {
|
|
72
|
+
zeroAccessPaths: [],
|
|
73
|
+
readOnlyPaths: [],
|
|
74
|
+
noDeletePaths: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let currentSection = null;
|
|
78
|
+
|
|
79
|
+
const lines = content.split('\n');
|
|
80
|
+
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (line.trim().startsWith('#') || line.trim() === '') continue;
|
|
83
|
+
|
|
84
|
+
if (line.match(/^zeroAccessPaths:/)) {
|
|
85
|
+
currentSection = 'zeroAccessPaths';
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (line.match(/^readOnlyPaths:/)) {
|
|
89
|
+
currentSection = 'readOnlyPaths';
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (line.match(/^noDeletePaths:/)) {
|
|
93
|
+
currentSection = 'noDeletePaths';
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (line.match(/^(bashToolPatterns|askPatterns|agileflowPatterns|config):/)) {
|
|
97
|
+
currentSection = null;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (currentSection && rules[currentSection]) {
|
|
102
|
+
const pathMatch = line.match(/^\s+-\s*['"]?(.+?)['"]?\s*$/);
|
|
103
|
+
if (pathMatch) {
|
|
104
|
+
rules[currentSection].push(pathMatch[1]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return rules;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Default path rules if patterns.yaml not found
|
|
114
|
+
*/
|
|
115
|
+
function getDefaultPathRules() {
|
|
116
|
+
return {
|
|
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'],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Expand home directory in path
|
|
125
|
+
*/
|
|
126
|
+
function expandHome(filePath) {
|
|
127
|
+
if (filePath.startsWith('~/')) {
|
|
128
|
+
return path.join(process.env.HOME || '', filePath.slice(2));
|
|
129
|
+
}
|
|
130
|
+
return filePath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a file path matches a pattern
|
|
135
|
+
*/
|
|
136
|
+
function pathMatches(filePath, pattern) {
|
|
137
|
+
const expandedPattern = expandHome(pattern);
|
|
138
|
+
const normalizedFile = path.normalize(filePath);
|
|
139
|
+
const normalizedPattern = path.normalize(expandedPattern);
|
|
140
|
+
|
|
141
|
+
// Exact match
|
|
142
|
+
if (normalizedFile === normalizedPattern) return true;
|
|
143
|
+
|
|
144
|
+
// Directory prefix match
|
|
145
|
+
if (pattern.endsWith('/')) {
|
|
146
|
+
if (normalizedFile.startsWith(normalizedPattern)) return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Glob pattern (**)
|
|
150
|
+
if (pattern.includes('**/')) {
|
|
151
|
+
const globPart = pattern.split('**/')[1];
|
|
152
|
+
if (normalizedFile.includes(globPart)) return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Wildcard at end
|
|
156
|
+
if (pattern.endsWith('*')) {
|
|
157
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
158
|
+
if (normalizedFile.startsWith(prefix)) return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Basename match
|
|
162
|
+
if (!pattern.includes('/') && !pattern.includes(path.sep)) {
|
|
163
|
+
const basename = path.basename(normalizedFile);
|
|
164
|
+
if (basename === pattern) return true;
|
|
165
|
+
if (pattern.endsWith('*')) {
|
|
166
|
+
const patternBase = pattern.slice(0, -1);
|
|
167
|
+
if (basename.startsWith(patternBase)) return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if file path is protected for writing
|
|
176
|
+
* Returns: { blocked: boolean, reason: string, level: string }
|
|
177
|
+
*/
|
|
178
|
+
function checkPath(filePath, rules) {
|
|
179
|
+
// Check zero access paths (blocked completely)
|
|
180
|
+
for (const pattern of rules.zeroAccessPaths) {
|
|
181
|
+
if (pathMatches(filePath, pattern)) {
|
|
182
|
+
return {
|
|
183
|
+
blocked: true,
|
|
184
|
+
reason: `Path is in zero-access protected list: ${pattern}`,
|
|
185
|
+
level: 'zero-access',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check read-only paths (cannot write)
|
|
191
|
+
for (const pattern of rules.readOnlyPaths) {
|
|
192
|
+
if (pathMatches(filePath, pattern)) {
|
|
193
|
+
return {
|
|
194
|
+
blocked: true,
|
|
195
|
+
reason: `Path is read-only: ${pattern}`,
|
|
196
|
+
level: 'read-only',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// noDeletePaths allows writing, only blocks deletion
|
|
202
|
+
// so we don't block writes here
|
|
203
|
+
|
|
204
|
+
return { blocked: false, reason: null, level: null };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Main entry point
|
|
209
|
+
*/
|
|
210
|
+
function main() {
|
|
211
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT;
|
|
212
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
213
|
+
|
|
214
|
+
if (!toolInput) {
|
|
215
|
+
process.exit(EXIT_ALLOW);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let input;
|
|
219
|
+
try {
|
|
220
|
+
input = JSON.parse(toolInput);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error('Error parsing CLAUDE_TOOL_INPUT:', e.message);
|
|
223
|
+
process.exit(EXIT_ALLOW);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const filePath = input.file_path;
|
|
227
|
+
if (!filePath) {
|
|
228
|
+
process.exit(EXIT_ALLOW);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Resolve to absolute path
|
|
232
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
233
|
+
|
|
234
|
+
// Load rules
|
|
235
|
+
const rules = loadPathRules(projectDir);
|
|
236
|
+
|
|
237
|
+
// Check path
|
|
238
|
+
const result = checkPath(absolutePath, rules);
|
|
239
|
+
|
|
240
|
+
if (result.blocked) {
|
|
241
|
+
console.error(`${c.red}${c.bold}BLOCKED${c.reset}: ${result.reason}`);
|
|
242
|
+
console.error(`${c.yellow}File: ${filePath}${c.reset}`);
|
|
243
|
+
console.error(`${c.cyan}This file is protected by damage control (${result.level}).${c.reset}`);
|
|
244
|
+
process.exit(EXIT_BLOCK);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
process.exit(EXIT_ALLOW);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (require.main === module) {
|
|
251
|
+
main();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = { checkPath, loadPathRules, pathMatches };
|
|
@@ -17,14 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
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
|
-
};
|
|
20
|
+
const { c } = require('../lib/colors');
|
|
28
21
|
|
|
29
22
|
/**
|
|
30
23
|
* Find project root by looking for .agileflow directory
|
|
@@ -48,7 +41,7 @@ function parseSimpleYAML(content) {
|
|
|
48
41
|
const config = {
|
|
49
42
|
bashToolPatterns: [],
|
|
50
43
|
askPatterns: [],
|
|
51
|
-
agileflowProtections: []
|
|
44
|
+
agileflowProtections: [],
|
|
52
45
|
};
|
|
53
46
|
|
|
54
47
|
let currentSection = null;
|
|
@@ -76,13 +69,22 @@ function parseSimpleYAML(content) {
|
|
|
76
69
|
currentPattern = null;
|
|
77
70
|
} else if (trimmed.startsWith('- pattern:') && currentSection) {
|
|
78
71
|
// New pattern entry
|
|
79
|
-
const patternValue = trimmed
|
|
72
|
+
const patternValue = trimmed
|
|
73
|
+
.replace('- pattern:', '')
|
|
74
|
+
.trim()
|
|
75
|
+
.replace(/^["']|["']$/g, '');
|
|
80
76
|
currentPattern = { pattern: patternValue };
|
|
81
77
|
config[currentSection].push(currentPattern);
|
|
82
78
|
} else if (trimmed.startsWith('reason:') && currentPattern) {
|
|
83
|
-
currentPattern.reason = trimmed
|
|
79
|
+
currentPattern.reason = trimmed
|
|
80
|
+
.replace('reason:', '')
|
|
81
|
+
.trim()
|
|
82
|
+
.replace(/^["']|["']$/g, '');
|
|
84
83
|
} else if (trimmed.startsWith('flags:') && currentPattern) {
|
|
85
|
-
currentPattern.flags = trimmed
|
|
84
|
+
currentPattern.flags = trimmed
|
|
85
|
+
.replace('flags:', '')
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/^["']|["']$/g, '');
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -96,7 +98,7 @@ function loadPatterns(projectRoot) {
|
|
|
96
98
|
const configPaths = [
|
|
97
99
|
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
98
100
|
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
99
|
-
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
|
|
101
|
+
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
|
|
100
102
|
];
|
|
101
103
|
|
|
102
104
|
for (const configPath of configPaths) {
|
|
@@ -135,14 +137,14 @@ function validateCommand(command, config) {
|
|
|
135
137
|
// Check blocked patterns (bashToolPatterns + agileflowProtections)
|
|
136
138
|
const blockedPatterns = [
|
|
137
139
|
...(config.bashToolPatterns || []),
|
|
138
|
-
...(config.agileflowProtections || [])
|
|
140
|
+
...(config.agileflowProtections || []),
|
|
139
141
|
];
|
|
140
142
|
|
|
141
143
|
for (const rule of blockedPatterns) {
|
|
142
144
|
if (matchesPattern(command, rule)) {
|
|
143
145
|
return {
|
|
144
146
|
action: 'block',
|
|
145
|
-
reason: rule.reason || 'Command blocked by damage control'
|
|
147
|
+
reason: rule.reason || 'Command blocked by damage control',
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
150
|
}
|
|
@@ -152,7 +154,7 @@ function validateCommand(command, config) {
|
|
|
152
154
|
if (matchesPattern(command, rule)) {
|
|
153
155
|
return {
|
|
154
156
|
action: 'ask',
|
|
155
|
-
reason: rule.reason || 'Please confirm this command'
|
|
157
|
+
reason: rule.reason || 'Please confirm this command',
|
|
156
158
|
};
|
|
157
159
|
}
|
|
158
160
|
}
|
|
@@ -192,17 +194,21 @@ function main() {
|
|
|
192
194
|
switch (result.action) {
|
|
193
195
|
case 'block':
|
|
194
196
|
// Output error message and block
|
|
195
|
-
console.error(`${c.
|
|
196
|
-
console.error(
|
|
197
|
+
console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
|
|
198
|
+
console.error(
|
|
199
|
+
`${c.dim}Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}${c.reset}`
|
|
200
|
+
);
|
|
197
201
|
process.exit(2);
|
|
198
202
|
break;
|
|
199
203
|
|
|
200
204
|
case 'ask':
|
|
201
205
|
// Output JSON to trigger user confirmation
|
|
202
|
-
console.log(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
console.log(
|
|
207
|
+
JSON.stringify({
|
|
208
|
+
result: 'ask',
|
|
209
|
+
message: result.reason,
|
|
210
|
+
})
|
|
211
|
+
);
|
|
206
212
|
process.exit(0);
|
|
207
213
|
break;
|
|
208
214
|
|
|
@@ -15,13 +15,7 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
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
|
-
};
|
|
18
|
+
const { c } = require('../lib/colors');
|
|
25
19
|
|
|
26
20
|
/**
|
|
27
21
|
* Find project root by looking for .agileflow directory
|
|
@@ -54,7 +48,7 @@ function parseSimpleYAML(content) {
|
|
|
54
48
|
const config = {
|
|
55
49
|
zeroAccessPaths: [],
|
|
56
50
|
readOnlyPaths: [],
|
|
57
|
-
noDeletePaths: []
|
|
51
|
+
noDeletePaths: [],
|
|
58
52
|
};
|
|
59
53
|
|
|
60
54
|
let currentSection = null;
|
|
@@ -92,7 +86,7 @@ function loadPatterns(projectRoot) {
|
|
|
92
86
|
const configPaths = [
|
|
93
87
|
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
94
88
|
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
95
|
-
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
|
|
89
|
+
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
|
|
96
90
|
];
|
|
97
91
|
|
|
98
92
|
for (const configPath of configPaths) {
|
|
@@ -168,7 +162,7 @@ function validatePath(filePath, config) {
|
|
|
168
162
|
return {
|
|
169
163
|
action: 'block',
|
|
170
164
|
reason: `Zero-access path: ${zeroMatch}`,
|
|
171
|
-
detail: 'This file is protected and cannot be accessed'
|
|
165
|
+
detail: 'This file is protected and cannot be accessed',
|
|
172
166
|
};
|
|
173
167
|
}
|
|
174
168
|
|
|
@@ -178,7 +172,7 @@ function validatePath(filePath, config) {
|
|
|
178
172
|
return {
|
|
179
173
|
action: 'block',
|
|
180
174
|
reason: `Read-only path: ${readOnlyMatch}`,
|
|
181
|
-
detail: 'This file is read-only and cannot be edited'
|
|
175
|
+
detail: 'This file is read-only and cannot be edited',
|
|
182
176
|
};
|
|
183
177
|
}
|
|
184
178
|
|
|
@@ -215,7 +209,7 @@ function main() {
|
|
|
215
209
|
const result = validatePath(filePath, config);
|
|
216
210
|
|
|
217
211
|
if (result.action === 'block') {
|
|
218
|
-
console.error(`${c.
|
|
212
|
+
console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
|
|
219
213
|
console.error(`${c.dim}${result.detail}${c.reset}`);
|
|
220
214
|
console.error(`${c.dim}File: ${filePath}${c.reset}`);
|
|
221
215
|
process.exit(2);
|