agileflow 2.87.0 → 2.89.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/lib/validate.js CHANGED
@@ -2,9 +2,12 @@
2
2
  * AgileFlow CLI - Input Validation Utilities
3
3
  *
4
4
  * Centralized validation patterns and helpers to prevent
5
- * command injection and invalid input handling.
5
+ * command injection, path traversal, and invalid input handling.
6
6
  */
7
7
 
8
+ const path = require('node:path');
9
+ const fs = require('node:fs');
10
+
8
11
  /**
9
12
  * Validation patterns for common input types.
10
13
  * All patterns use strict whitelisting approach.
@@ -320,7 +323,277 @@ function validateArgs(args, schema) {
320
323
  return { ok: true, data: result };
321
324
  }
322
325
 
326
+ // =============================================================================
327
+ // Path Traversal Protection
328
+ // =============================================================================
329
+
330
+ /**
331
+ * Path validation error with context.
332
+ */
333
+ class PathValidationError extends Error {
334
+ /**
335
+ * @param {string} message - Error message
336
+ * @param {string} inputPath - The problematic path
337
+ * @param {string} reason - Reason for rejection
338
+ */
339
+ constructor(message, inputPath, reason) {
340
+ super(message);
341
+ this.name = 'PathValidationError';
342
+ this.inputPath = inputPath;
343
+ this.reason = reason;
344
+ Error.captureStackTrace(this, this.constructor);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Validate that a path is safe and within the allowed base directory.
350
+ * Prevents path traversal attacks by:
351
+ * 1. Resolving the path to absolute form
352
+ * 2. Ensuring it stays within the base directory
353
+ * 3. Rejecting symbolic links (optional)
354
+ *
355
+ * @param {string} inputPath - The path to validate (can be relative or absolute)
356
+ * @param {string} baseDir - The allowed base directory (must be absolute)
357
+ * @param {Object} options - Validation options
358
+ * @param {boolean} [options.allowSymlinks=false] - Allow symbolic links
359
+ * @param {boolean} [options.mustExist=false] - Path must exist on filesystem
360
+ * @returns {{ ok: boolean, resolvedPath?: string, error?: PathValidationError }}
361
+ *
362
+ * @example
363
+ * // Validate a file path within project directory
364
+ * const result = validatePath('./config.yaml', '/home/user/project');
365
+ * if (result.ok) {
366
+ * console.log('Safe path:', result.resolvedPath);
367
+ * }
368
+ *
369
+ * @example
370
+ * // Reject path traversal attempt
371
+ * const result = validatePath('../../../etc/passwd', '/home/user/project');
372
+ * // result.ok === false
373
+ * // result.error.reason === 'path_traversal'
374
+ */
375
+ function validatePath(inputPath, baseDir, options = {}) {
376
+ const { allowSymlinks = false, mustExist = false } = options;
377
+
378
+ // Input validation
379
+ if (!inputPath || typeof inputPath !== 'string') {
380
+ return {
381
+ ok: false,
382
+ error: new PathValidationError(
383
+ 'Path is required and must be a string',
384
+ String(inputPath),
385
+ 'invalid_input'
386
+ ),
387
+ };
388
+ }
389
+
390
+ if (!baseDir || typeof baseDir !== 'string') {
391
+ return {
392
+ ok: false,
393
+ error: new PathValidationError(
394
+ 'Base directory is required and must be a string',
395
+ inputPath,
396
+ 'invalid_base'
397
+ ),
398
+ };
399
+ }
400
+
401
+ // Base directory must be absolute
402
+ if (!path.isAbsolute(baseDir)) {
403
+ return {
404
+ ok: false,
405
+ error: new PathValidationError(
406
+ 'Base directory must be an absolute path',
407
+ inputPath,
408
+ 'relative_base'
409
+ ),
410
+ };
411
+ }
412
+
413
+ // Normalize the base directory
414
+ const normalizedBase = path.resolve(baseDir);
415
+
416
+ // Resolve the input path relative to base directory
417
+ let resolvedPath;
418
+ if (path.isAbsolute(inputPath)) {
419
+ resolvedPath = path.resolve(inputPath);
420
+ } else {
421
+ resolvedPath = path.resolve(normalizedBase, inputPath);
422
+ }
423
+
424
+ // Check for path traversal: resolved path must start with base directory
425
+ // Add trailing separator to prevent prefix attacks (e.g., /home/user vs /home/user2)
426
+ const baseWithSep = normalizedBase.endsWith(path.sep)
427
+ ? normalizedBase
428
+ : normalizedBase + path.sep;
429
+
430
+ const isWithinBase =
431
+ resolvedPath === normalizedBase || resolvedPath.startsWith(baseWithSep);
432
+
433
+ if (!isWithinBase) {
434
+ return {
435
+ ok: false,
436
+ error: new PathValidationError(
437
+ `Path escapes base directory: ${inputPath}`,
438
+ inputPath,
439
+ 'path_traversal'
440
+ ),
441
+ };
442
+ }
443
+
444
+ // Check if path exists (if required)
445
+ if (mustExist) {
446
+ try {
447
+ fs.accessSync(resolvedPath);
448
+ } catch {
449
+ return {
450
+ ok: false,
451
+ error: new PathValidationError(
452
+ `Path does not exist: ${resolvedPath}`,
453
+ inputPath,
454
+ 'not_found'
455
+ ),
456
+ };
457
+ }
458
+ }
459
+
460
+ // Check for symbolic links (if not allowed)
461
+ if (!allowSymlinks) {
462
+ try {
463
+ const stats = fs.lstatSync(resolvedPath);
464
+ if (stats.isSymbolicLink()) {
465
+ return {
466
+ ok: false,
467
+ error: new PathValidationError(
468
+ `Symbolic links are not allowed: ${inputPath}`,
469
+ inputPath,
470
+ 'symlink_rejected'
471
+ ),
472
+ };
473
+ }
474
+ } catch {
475
+ // Path doesn't exist yet, which is fine if mustExist is false
476
+ // Check parent directories for symlinks
477
+ const parts = path.relative(normalizedBase, resolvedPath).split(path.sep);
478
+ let currentPath = normalizedBase;
479
+
480
+ for (const part of parts) {
481
+ currentPath = path.join(currentPath, part);
482
+ try {
483
+ const stats = fs.lstatSync(currentPath);
484
+ if (stats.isSymbolicLink()) {
485
+ return {
486
+ ok: false,
487
+ error: new PathValidationError(
488
+ `Path contains symbolic link: ${currentPath}`,
489
+ inputPath,
490
+ 'symlink_in_path'
491
+ ),
492
+ };
493
+ }
494
+ } catch {
495
+ // Part of path doesn't exist, stop checking
496
+ break;
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+ return {
503
+ ok: true,
504
+ resolvedPath,
505
+ };
506
+ }
507
+
508
+ /**
509
+ * Synchronous version that throws on invalid paths.
510
+ * Use when you want exceptions rather than result objects.
511
+ *
512
+ * @param {string} inputPath - The path to validate
513
+ * @param {string} baseDir - The allowed base directory
514
+ * @param {Object} options - Validation options
515
+ * @returns {string} The validated absolute path
516
+ * @throws {PathValidationError} If path is invalid
517
+ */
518
+ function validatePathSync(inputPath, baseDir, options = {}) {
519
+ const result = validatePath(inputPath, baseDir, options);
520
+ if (!result.ok) {
521
+ throw result.error;
522
+ }
523
+ return result.resolvedPath;
524
+ }
525
+
526
+ /**
527
+ * Check if a path contains dangerous patterns without resolving.
528
+ * Useful for quick pre-validation before expensive operations.
529
+ *
530
+ * @param {string} inputPath - The path to check
531
+ * @returns {{ safe: boolean, reason?: string }}
532
+ */
533
+ function hasUnsafePathPatterns(inputPath) {
534
+ if (!inputPath || typeof inputPath !== 'string') {
535
+ return { safe: false, reason: 'invalid_input' };
536
+ }
537
+
538
+ // Check for null bytes (can bypass security in some systems)
539
+ if (inputPath.includes('\0')) {
540
+ return { safe: false, reason: 'null_byte' };
541
+ }
542
+
543
+ // Check for obvious traversal patterns
544
+ if (inputPath.includes('..')) {
545
+ return { safe: false, reason: 'dot_dot_sequence' };
546
+ }
547
+
548
+ // Check for absolute paths on Unix when expecting relative
549
+ if (inputPath.startsWith('/') && !path.isAbsolute(inputPath)) {
550
+ return { safe: false, reason: 'unexpected_absolute' };
551
+ }
552
+
553
+ // Check for Windows-style absolute paths
554
+ if (/^[a-zA-Z]:/.test(inputPath)) {
555
+ return { safe: false, reason: 'windows_absolute' };
556
+ }
557
+
558
+ return { safe: true };
559
+ }
560
+
561
+ /**
562
+ * Sanitize a filename by removing dangerous characters.
563
+ * Does NOT validate the full path - use with validatePath().
564
+ *
565
+ * @param {string} filename - The filename to sanitize
566
+ * @param {Object} options - Sanitization options
567
+ * @param {string} [options.replacement='_'] - Character to replace with
568
+ * @param {number} [options.maxLength=255] - Maximum filename length
569
+ * @returns {string} Sanitized filename
570
+ */
571
+ function sanitizeFilename(filename, options = {}) {
572
+ const { replacement = '_', maxLength = 255 } = options;
573
+
574
+ if (!filename || typeof filename !== 'string') {
575
+ return '';
576
+ }
577
+
578
+ // Remove or replace dangerous characters
579
+ let sanitized = filename
580
+ .replace(/[<>:"/\\|?*\x00-\x1f]/g, replacement) // Control chars and reserved
581
+ .replace(/\.{2,}/g, replacement) // Multiple dots
582
+ .replace(/^\.+/, replacement) // Leading dots
583
+ .replace(/^-+/, replacement); // Leading dashes (prevent flag injection)
584
+
585
+ // Truncate if too long
586
+ if (sanitized.length > maxLength) {
587
+ const ext = path.extname(sanitized);
588
+ const base = path.basename(sanitized, ext);
589
+ sanitized = base.slice(0, maxLength - ext.length) + ext;
590
+ }
591
+
592
+ return sanitized;
593
+ }
594
+
323
595
  module.exports = {
596
+ // Patterns and basic validators
324
597
  PATTERNS,
325
598
  isValidBranchName,
326
599
  isValidStoryId,
@@ -334,4 +607,11 @@ module.exports = {
334
607
  parseIntBounded,
335
608
  isValidOption,
336
609
  validateArgs,
610
+
611
+ // Path traversal protection
612
+ PathValidationError,
613
+ validatePath,
614
+ validatePathSync,
615
+ hasUnsafePathPatterns,
616
+ sanitizeFilename,
337
617
  };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Centralized YAML parsing utilities with security guarantees.
3
+ *
4
+ * Security Note (js-yaml v4.x):
5
+ * - yaml.load() is SAFE by default in v4+ (unlike v3.x)
6
+ * - Dangerous JavaScript types (functions, regexps) are NOT supported
7
+ * - Only JSON-compatible types are parsed
8
+ * - See: https://github.com/nodeca/js-yaml/blob/master/migrate_v3_to_v4.md
9
+ *
10
+ * This wrapper provides:
11
+ * - Explicit security documentation
12
+ * - Input validation
13
+ * - Consistent error handling
14
+ * - Future-proofing for library changes
15
+ */
16
+
17
+ const yaml = require('js-yaml');
18
+
19
+ /**
20
+ * Safely parse YAML content. Uses js-yaml's DEFAULT_SCHEMA which is
21
+ * safe by default in v4+ (does not execute arbitrary JavaScript).
22
+ *
23
+ * @param {string} content - YAML string to parse
24
+ * @param {Object} options - Optional yaml.load options
25
+ * @returns {any} Parsed YAML content (object, array, or primitive)
26
+ * @throws {yaml.YAMLException} If YAML is malformed
27
+ */
28
+ function safeLoad(content, options = {}) {
29
+ if (typeof content !== 'string') {
30
+ throw new TypeError('YAML content must be a string');
31
+ }
32
+
33
+ // In js-yaml v4+, load() uses DEFAULT_SCHEMA which is safe.
34
+ // Explicitly pass schema to make the security guarantee clear
35
+ // and protect against future library changes.
36
+ return yaml.load(content, {
37
+ schema: yaml.DEFAULT_SCHEMA,
38
+ ...options,
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Safely parse all YAML documents in a multi-document file.
44
+ *
45
+ * @param {string} content - YAML string containing one or more documents
46
+ * @param {Object} options - Optional yaml.loadAll options
47
+ * @returns {any[]} Array of parsed YAML documents
48
+ * @throws {yaml.YAMLException} If YAML is malformed
49
+ */
50
+ function safeLoadAll(content, options = {}) {
51
+ if (typeof content !== 'string') {
52
+ throw new TypeError('YAML content must be a string');
53
+ }
54
+
55
+ const documents = [];
56
+ yaml.loadAll(
57
+ content,
58
+ (doc) => documents.push(doc),
59
+ { schema: yaml.DEFAULT_SCHEMA, ...options }
60
+ );
61
+ return documents;
62
+ }
63
+
64
+ /**
65
+ * Safely serialize data to YAML string.
66
+ *
67
+ * @param {any} data - Data to serialize
68
+ * @param {Object} options - Optional yaml.dump options
69
+ * @returns {string} YAML string representation
70
+ */
71
+ function safeDump(data, options = {}) {
72
+ return yaml.dump(data, {
73
+ schema: yaml.DEFAULT_SCHEMA,
74
+ ...options,
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Test if js-yaml is configured securely (no JavaScript type support).
80
+ * This function is used in security tests to verify the library version
81
+ * and configuration.
82
+ *
83
+ * @returns {boolean} True if the configuration is secure
84
+ */
85
+ function isSecureConfiguration() {
86
+ // Attempt to parse YAML with JavaScript function syntax
87
+ // This should NOT execute any code in v4+
88
+ const maliciousYaml = `
89
+ test: !!js/function 'function() { return "pwned"; }'
90
+ regex: !!js/regexp /test/i
91
+ undef: !!js/undefined ''
92
+ `;
93
+
94
+ try {
95
+ const result = safeLoad(maliciousYaml);
96
+ // If we get here without executing code, and the values are
97
+ // either strings or undefined (not actual functions), we're safe
98
+ if (typeof result.test === 'function') {
99
+ return false; // Unsafe: function was instantiated
100
+ }
101
+ if (result.regex instanceof RegExp) {
102
+ return false; // Unsafe: regex was instantiated
103
+ }
104
+ // Values should be undefined or error out due to unknown tags
105
+ return true;
106
+ } catch (e) {
107
+ // YAMLException for unknown tags is the expected safe behavior
108
+ if (e.name === 'YAMLException') {
109
+ return true;
110
+ }
111
+ throw e;
112
+ }
113
+ }
114
+
115
+ module.exports = {
116
+ safeLoad,
117
+ safeLoadAll,
118
+ safeDump,
119
+ isSecureConfiguration,
120
+ // Re-export yaml module for edge cases that need direct access
121
+ yaml,
122
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.87.0",
3
+ "version": "2.89.0",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -53,8 +53,6 @@
53
53
  "test:coverage": "jest --coverage"
54
54
  },
55
55
  "dependencies": {
56
- "blessed": "^0.1.81",
57
- "blessed-contrib": "^4.10.1",
58
56
  "chalk": "^4.1.2",
59
57
  "commander": "^12.1.0",
60
58
  "fs-extra": "^11.2.0",
@@ -18,6 +18,7 @@ const { execSync, spawnSync } = require('child_process');
18
18
  // Shared utilities
19
19
  const { c, box } = require('../lib/colors');
20
20
  const { getProjectRoot } = require('../lib/paths');
21
+ const { readJSONCached, readFileCached } = require('../lib/file-cache');
21
22
 
22
23
  // Session manager path (relative to script location)
23
24
  const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
@@ -47,44 +48,29 @@ try {
47
48
  }
48
49
 
49
50
  /**
50
- * PERFORMANCE OPTIMIZATION: Load all project files once into cache
51
- * This eliminates duplicate file reads across multiple functions.
52
- * Estimated savings: 40-80ms
51
+ * PERFORMANCE OPTIMIZATION: Load all project files using LRU cache
52
+ * Uses file-cache module for automatic caching with 30s TTL.
53
+ * Files are cached across script invocations within TTL window.
54
+ * Estimated savings: 60-120ms on cache hits
53
55
  */
54
56
  function loadProjectFiles(rootDir) {
55
- const cache = {
56
- status: null,
57
- metadata: null,
58
- settings: null,
59
- sessionState: null,
60
- configYaml: null,
61
- cliPackage: null,
62
- };
63
-
64
57
  const paths = {
65
- status: 'docs/09-agents/status.json',
66
- metadata: 'docs/00-meta/agileflow-metadata.json',
67
- settings: '.claude/settings.json',
68
- sessionState: 'docs/09-agents/session-state.json',
69
- configYaml: '.agileflow/config.yaml',
70
- cliPackage: 'packages/cli/package.json',
58
+ status: path.join(rootDir, 'docs', '09-agents', 'status.json'),
59
+ metadata: path.join(rootDir, 'docs', '00-meta', 'agileflow-metadata.json'),
60
+ settings: path.join(rootDir, '.claude', 'settings.json'),
61
+ sessionState: path.join(rootDir, 'docs', '09-agents', 'session-state.json'),
62
+ configYaml: path.join(rootDir, '.agileflow', 'config.yaml'),
63
+ cliPackage: path.join(rootDir, 'packages', 'cli', 'package.json'),
71
64
  };
72
65
 
73
- for (const [key, relPath] of Object.entries(paths)) {
74
- const fullPath = path.join(rootDir, relPath);
75
- try {
76
- if (!fs.existsSync(fullPath)) continue;
77
- if (key === 'configYaml') {
78
- cache[key] = fs.readFileSync(fullPath, 'utf8');
79
- } else {
80
- cache[key] = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
81
- }
82
- } catch (e) {
83
- // Silently ignore - file not available or invalid
84
- }
85
- }
86
-
87
- return cache;
66
+ return {
67
+ status: readJSONCached(paths.status),
68
+ metadata: readJSONCached(paths.metadata),
69
+ settings: readJSONCached(paths.settings),
70
+ sessionState: readJSONCached(paths.sessionState),
71
+ configYaml: readFileCached(paths.configYaml),
72
+ cliPackage: readJSONCached(paths.cliPackage),
73
+ };
88
74
  }
89
75
 
90
76
  /**
@@ -20,67 +20,10 @@ const {
20
20
  loadPatterns,
21
21
  outputBlocked,
22
22
  runDamageControlHook,
23
+ parseBashPatterns,
23
24
  c,
24
25
  } = require('./lib/damage-control-utils');
25
26
 
26
- /**
27
- * Parse simplified YAML for damage control patterns
28
- * Only parses the structure we need - not a full YAML parser
29
- */
30
- function parseSimpleYAML(content) {
31
- const config = {
32
- bashToolPatterns: [],
33
- askPatterns: [],
34
- agileflowProtections: [],
35
- };
36
-
37
- let currentSection = null;
38
- let currentPattern = null;
39
-
40
- for (const line of content.split('\n')) {
41
- const trimmed = line.trim();
42
-
43
- // Skip empty lines and comments
44
- if (!trimmed || trimmed.startsWith('#')) continue;
45
-
46
- // Detect section headers
47
- if (trimmed === 'bashToolPatterns:') {
48
- currentSection = 'bashToolPatterns';
49
- currentPattern = null;
50
- } else if (trimmed === 'askPatterns:') {
51
- currentSection = 'askPatterns';
52
- currentPattern = null;
53
- } else if (trimmed === 'agileflowProtections:') {
54
- currentSection = 'agileflowProtections';
55
- currentPattern = null;
56
- } else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
57
- // Other sections we don't care about for bash validation
58
- currentSection = null;
59
- currentPattern = null;
60
- } else if (trimmed.startsWith('- pattern:') && currentSection) {
61
- // New pattern entry
62
- const patternValue = trimmed
63
- .replace('- pattern:', '')
64
- .trim()
65
- .replace(/^["']|["']$/g, '');
66
- currentPattern = { pattern: patternValue };
67
- config[currentSection].push(currentPattern);
68
- } else if (trimmed.startsWith('reason:') && currentPattern) {
69
- currentPattern.reason = trimmed
70
- .replace('reason:', '')
71
- .trim()
72
- .replace(/^["']|["']$/g, '');
73
- } else if (trimmed.startsWith('flags:') && currentPattern) {
74
- currentPattern.flags = trimmed
75
- .replace('flags:', '')
76
- .trim()
77
- .replace(/^["']|["']$/g, '');
78
- }
79
- }
80
-
81
- return config;
82
- }
83
-
84
27
  /**
85
28
  * Test command against a single pattern rule
86
29
  */
@@ -134,7 +77,7 @@ const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtecti
134
77
 
135
78
  runDamageControlHook({
136
79
  getInputValue: input => input.command || input.tool_input?.command,
137
- loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
80
+ loadConfig: () => loadPatterns(projectRoot, parseBashPatterns, defaultConfig),
138
81
  validate: validateCommand,
139
82
  onBlock: (result, command) => {
140
83
  outputBlocked(
@@ -15,85 +15,20 @@
15
15
  const {
16
16
  findProjectRoot,
17
17
  loadPatterns,
18
- pathMatches,
19
18
  outputBlocked,
20
19
  runDamageControlHook,
20
+ parsePathPatterns,
21
+ validatePathAgainstPatterns,
21
22
  } = require('./lib/damage-control-utils');
22
23
 
23
- /**
24
- * Parse simplified YAML for path patterns
25
- */
26
- function parseSimpleYAML(content) {
27
- const config = {
28
- zeroAccessPaths: [],
29
- readOnlyPaths: [],
30
- noDeletePaths: [],
31
- };
32
-
33
- let currentSection = null;
34
-
35
- for (const line of content.split('\n')) {
36
- const trimmed = line.trim();
37
-
38
- // Skip empty lines and comments
39
- if (!trimmed || trimmed.startsWith('#')) continue;
40
-
41
- // Detect section headers
42
- if (trimmed === 'zeroAccessPaths:') {
43
- currentSection = 'zeroAccessPaths';
44
- } else if (trimmed === 'readOnlyPaths:') {
45
- currentSection = 'readOnlyPaths';
46
- } else if (trimmed === 'noDeletePaths:') {
47
- currentSection = 'noDeletePaths';
48
- } else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
49
- // Other sections we don't care about for path validation
50
- currentSection = null;
51
- } else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
52
- // Path entry
53
- const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
54
- config[currentSection].push(pathValue);
55
- }
56
- }
57
-
58
- return config;
59
- }
60
-
61
- /**
62
- * Validate file path for edit operation
63
- */
64
- function validatePath(filePath, config) {
65
- // Check zero access paths - completely blocked
66
- const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
67
- if (zeroMatch) {
68
- return {
69
- action: 'block',
70
- reason: `Zero-access path: ${zeroMatch}`,
71
- detail: 'This file is protected and cannot be accessed',
72
- };
73
- }
74
-
75
- // Check read-only paths - cannot edit
76
- const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
77
- if (readOnlyMatch) {
78
- return {
79
- action: 'block',
80
- reason: `Read-only path: ${readOnlyMatch}`,
81
- detail: 'This file is read-only and cannot be edited',
82
- };
83
- }
84
-
85
- // Allow by default
86
- return { action: 'allow' };
87
- }
88
-
89
24
  // Run the hook
90
25
  const projectRoot = findProjectRoot();
91
26
  const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
92
27
 
93
28
  runDamageControlHook({
94
29
  getInputValue: input => input.file_path || input.tool_input?.file_path,
95
- loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
96
- validate: validatePath,
30
+ loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
31
+ validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'edit'),
97
32
  onBlock: (result, filePath) => {
98
33
  outputBlocked(result.reason, result.detail, `File: ${filePath}`);
99
34
  },