trackfw 6.6.0 → 6.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "6.6.0",
3
+ "version": "6.7.0",
4
4
  "description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, Amazon Q, OpenCode, and Kiro.",
5
5
  "keywords": [
6
6
  "cli",
@@ -174,8 +174,6 @@ elif printf '%s' "$INPUT" | grep -qE "$AWS_KEY_PATTERN"; then
174
174
  MATCH="AWS access key"
175
175
  fi
176
176
 
177
- [ -n "$MATCH" ] || exit 0
178
-
179
177
  # The raw payload is JSON: any double quote inside the underlying tool_input.command is
180
178
  # escaped as \\" -- unescape those before scanning for redirect targets, or a quoted target
181
179
  # like "$TMPFILE" is seen as starting with a literal backslash instead of a variable
@@ -206,6 +204,57 @@ is_ephemeral_target() {
206
204
 
207
205
  REDIRECTS=$(printf '%s' "$RAW" | grep -oE '[0-9]?>>?[[:space:]]*[^[:space:]|&;,:]+' || true)
208
206
 
207
+ # Second detection layer: only runs when the payload scan above found nothing -- keeps the common
208
+ # case (match already found) cheap and avoids reading files unnecessarily. Files above the size cap
209
+ # are skipped silently.
210
+ scan_file_for_pattern() {
211
+ local path size
212
+ path=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
213
+ [ -n "$path" ] && [ -f "$path" ] || return 1
214
+ size=$(wc -c < "$path" 2>/dev/null | tr -d '[:space:]')
215
+ size=\${size:-0}
216
+ [ "$size" -lt 1048576 ] || return 1
217
+ if grep -qE "$JWT_PATTERN" "$path" 2>/dev/null; then
218
+ MATCH="JWT"
219
+ return 0
220
+ fi
221
+ if grep -qE "$AWS_KEY_PATTERN" "$path" 2>/dev/null; then
222
+ MATCH="AWS access key"
223
+ return 0
224
+ fi
225
+ return 1
226
+ }
227
+
228
+ if [ -z "$MATCH" ] && [ -n "$REDIRECTS" ]; then
229
+ while IFS= read -r line; do
230
+ if [ -z "$line" ]; then
231
+ continue
232
+ fi
233
+ target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
234
+ if ! is_ephemeral_target "$target"; then
235
+ scan_file_for_pattern "$target" && break
236
+ fi
237
+ done <<< "$REDIRECTS"
238
+ fi
239
+
240
+ if [ -z "$MATCH" ]; then
241
+ CMD_LINE=$(printf '%s' "$RAW" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
242
+ if [ -n "$CMD_LINE" ]; then
243
+ set -- $CMD_LINE
244
+ cmd_name="\${1:-}"
245
+ case "$cmd_name" in
246
+ cat|head|tail|jq|grep)
247
+ shift
248
+ for token in "$@"; do
249
+ scan_file_for_pattern "$token" && break
250
+ done
251
+ ;;
252
+ esac
253
+ fi
254
+ fi
255
+
256
+ [ -n "$MATCH" ] || exit 0
257
+
209
258
  HAS_REDIRECT=0
210
259
  EXEMPT=1
211
260
  if [ -n "$REDIRECTS" ]; then
@@ -227,10 +276,22 @@ fi
227
276
 
228
277
  `
229
278
 
230
- const CG_PROJECT_TAIL = `MODE=$(grep -A 5 '^credential_guard:' trackfw.yaml 2>/dev/null | grep 'mode:' | head -1 | sed -E 's/^[[:space:]]*mode:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d "\\"'" || true)
279
+ // Resolução de MODE (grep de `credential_guard.mode` em trackfw.yaml + fallback) é inlined,
280
+ // idêntica em CG_PROJECT_TAIL (fallback "warn") e CG_GLOBAL_TAIL (fallback "block") — ao invés de
281
+ // extrair para uma constante JS compartilhada e concatenar (como o Go faz via
282
+ // credentialGuardModeResolution/DEFAULT_MODE), aqui o texto é replicado como literal em cada
283
+ // template literal: o gate de paridade Go/Node/Python
284
+ // (internal/generators/credential_guard_test.go, getNodeSourceBlock) extrai cada constante via
285
+ // regex de um único bloco `` `const NAME = \`...\`` `` sem suportar concatenação de string —
286
+ // concatenar quebraria a extração estática. Nunca editar a lógica de resolução em só um dos dois
287
+ // blocos sem replicar no outro (ML-1B, ADR-2026-08-06 emenda 6). Mirrors Go's
288
+ // credentialGuardModeResolution (semântica idêntica, forma sintática diferente por essa restrição
289
+ // do parser de paridade).
290
+ const CG_PROJECT_TAIL = `DEFAULT_MODE="warn"
291
+ MODE=$(grep -A 5 '^credential_guard:' trackfw.yaml 2>/dev/null | grep 'mode:' | head -1 | sed -E 's/^[[:space:]]*mode:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d "\\"'" || true)
231
292
  case "$MODE" in
232
293
  warn|block) ;;
233
- *) MODE="warn" ;;
294
+ *) MODE="$DEFAULT_MODE" ;;
234
295
  esac
235
296
 
236
297
  if [ "$MODE" = "block" ]; then
@@ -259,10 +320,42 @@ printf '{"tool":"credential-guard","message":"%s","level":"action_required","tim
259
320
  exit 0
260
321
  `
261
322
 
262
- // Escopo global: modo sempre "warn" (ver ADR-2026-08-06/ROADMAP-2026-08-06 Wave 1 — decisão "b",
263
- // sem ~/.trackfw/config.yaml) e ROADMAP_DIR fixo em "docs/roadmaps" relativo ao cwd, só grava o
264
- // attention signal se esse diretório já existir (não cria docs/roadmaps num projeto qualquer).
265
- const CG_GLOBAL_TAIL = `MODE="warn"
323
+ // CG_GLOBAL_TAIL é a contraparte de CG_PROJECT_TAIL para o escopo global
324
+ // (~/.trackfw/scripts/trackfw-credential-guard.sh, instalado via `trackfw update harness`).
325
+ //
326
+ // Decisão (ML-1B, ver ADR-2026-08-06 emenda 6 de 2026-08-08 e ROADMAP-2026-08-08, Wave 1): o modo
327
+ // em escopo global reusa a MESMA leitura de `credential_guard.mode` de trackfw.yaml que
328
+ // CG_PROJECT_TAIL já faz (mesma resolução, replicada aqui — ver o comentário de CG_PROJECT_TAIL
329
+ // sobre por que não é extraída para uma constante compartilhada em Node) — sem exigir trackfw.yaml
330
+ // existir (não há o guard `[ -f trackfw.yaml ] || exit 0` da variante de projeto: o objetivo do
331
+ // escopo global é proteger qualquer projeto, com ou sem trackfw.yaml). Quando o hook global roda a
332
+ // partir do cwd de um projeto com trackfw.yaml e credential_guard.mode explícito, esse valor é
333
+ // respeitado (warn ou block) — nenhuma mudança de comportamento para quem já definiu mode: warn
334
+ // explicitamente. Em qualquer outro caso (sem trackfw.yaml, ou trackfw.yaml sem essa chave), o
335
+ // fallback deixa de ser "warn" e passa a ser "block": um guard opt-in que nunca bloqueia por
336
+ // padrão é uma falsa sensação de proteção — o usuário que rodou `trackfw update harness` já
337
+ // demonstrou intenção explícita de ter o mecanismo ativo. Supersede a decisão original ("modo
338
+ // global sempre warn", opção "b" avaliada na ADR original) — não cria ~/.trackfw/config.yaml nem
339
+ // nenhuma outra segunda fonte de configuração só para isto.
340
+ //
341
+ // ROADMAP_DIR em escopo global: como não há garantia de trackfw.yaml para ler `roadmap_dir:`, o
342
+ // script usa o caminho padrão fixo "docs/roadmaps" relativo ao cwd de onde o hook foi disparado, e
343
+ // só grava o attention signal se esse diretório já existir (e só em modo warn — modo block nunca
344
+ // grava o attention signal, mesma decisão da variante de projeto). Não cria "docs/roadmaps" em um
345
+ // projeto aleatório só para sinalizar isso — isso pareceria ao usuário que o trackfw foi
346
+ // "instalado" nesse projeto, o que não é verdade. O texto de warning/block em stderr acontece
347
+ // sempre (visível no output do CLI/hook), independente de o diretório de attention existir.
348
+ const CG_GLOBAL_TAIL = `DEFAULT_MODE="block"
349
+ MODE=$(grep -A 5 '^credential_guard:' trackfw.yaml 2>/dev/null | grep 'mode:' | head -1 | sed -E 's/^[[:space:]]*mode:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d "\\"'" || true)
350
+ case "$MODE" in
351
+ warn|block) ;;
352
+ *) MODE="$DEFAULT_MODE" ;;
353
+ esac
354
+
355
+ if [ "$MODE" = "block" ]; then
356
+ echo "trackfw-credential-guard: blocked - possible $MATCH detected in tool payload." >&2
357
+ exit 2
358
+ fi
266
359
 
267
360
  echo "trackfw-credential-guard: warning - possible $MATCH detected in tool payload." >&2
268
361
 
@@ -459,14 +552,21 @@ function injectClaudeHooks(cwd) {
459
552
 
460
553
  if (!data.hooks) data.hooks = {}
461
554
  data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'AskUserQuestion', SIGNAL_CMD)
462
- // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A): skip project-scope
463
- // credential-guard when the global one is already installed.
555
+ // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
556
+ // Wave 2 to Read/Write|Edit): skip project-scope credential-guard when the global one is
557
+ // already installed for this CLI.
464
558
  if (!globalCredentialGuardInstalledClaude()) {
465
559
  data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Bash', GUARD_CMD)
560
+ // ADR-2026-08-06 emenda 7 (2026-08-08): Read/Write/Edit coverage — extraction via direct
561
+ // file read, or materialization via write/edit, never went through the hook before.
562
+ data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Read', GUARD_CMD)
563
+ data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Write|Edit', GUARD_CMD)
466
564
  }
467
565
  data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'AskUserQuestion', CLEANUP_CMD)
468
566
  if (!globalCredentialGuardInstalledClaude()) {
469
567
  data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Bash', GUARD_CMD)
568
+ data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Read', GUARD_CMD)
569
+ data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Write|Edit', GUARD_CMD)
470
570
  }
471
571
 
472
572
  writeJSON(filePath, data)
@@ -483,6 +583,12 @@ function injectClaudeHooks(cwd) {
483
583
  // are enabled by default (no `[features] hooks = true`/`codex_hooks` opt-in
484
584
  // needed -- that flag exists only to turn hooks OFF), and PreToolUse blocking
485
585
  // uses exit code 2 + stderr (matching trackfw-credential-guard.sh's "block" mode).
586
+ //
587
+ // Read/Write/Edit coverage (ADR-2026-08-06 emenda 7, ROADMAP-2026-08-08 Wave 2, 2026-08-08):
588
+ // Codex has NO dedicated, interceptable read-tool matcher -- confirmed against
589
+ // https://learn.chatgpt.com/docs/hooks -- so no read matcher is added here; this is a documented
590
+ // limitation (also called out in docs/cli-parity.md), not a workaround. Write/edit materialization
591
+ // IS covered via the `apply_patch` matcher (documented aliases `Edit`/`Write`).
486
592
  // ---------------------------------------------------------------------------
487
593
 
488
594
  function injectCodexHooks(cwd) {
@@ -491,15 +597,18 @@ function injectCodexHooks(cwd) {
491
597
 
492
598
  if (!data.hooks) data.hooks = {}
493
599
  data.hooks.PermissionRequest = mergeClaudeHookArray(data.hooks.PermissionRequest, '.*', SIGNAL_CMD)
494
- // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A): skip project-scope
495
- // credential-guard when the global one is already installed.
600
+ // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
601
+ // Wave 2 to apply_patch): skip project-scope credential-guard when the global one is already
602
+ // installed for this CLI.
496
603
  const skipCodexCG = globalCredentialGuardInstalledCodex()
497
604
  if (!skipCodexCG) {
498
605
  data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Bash', GUARD_CMD)
606
+ data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'apply_patch', GUARD_CMD)
499
607
  }
500
608
  data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, '.*', CLEANUP_CMD)
501
609
  if (!skipCodexCG) {
502
610
  data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Bash', GUARD_CMD)
611
+ data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'apply_patch', GUARD_CMD)
503
612
  }
504
613
 
505
614
  writeJSON(filePath, data)
@@ -523,6 +632,12 @@ function injectCodexHooks(cwd) {
523
632
  // (matcher "*") is the pre-existing attention-cleanup wiring, unrelated to the new
524
633
  // credential-guard entry added as a separate array entry (different matcher) in the
525
634
  // same event.
635
+ //
636
+ // Read/Write/Edit coverage (ADR-2026-08-06 emenda 7, ROADMAP-2026-08-08 Wave 2, 2026-08-08): the
637
+ // Gemini CLI tools table (https://geminicli.com/docs/reference/tools) documents `read_file`/
638
+ // `read_many_files` as the file-read tools and `write_file`/`replace` as the file-write/edit
639
+ // tools -- matcher below follows the same regex-over-tool_name convention already used for
640
+ // `run_shell_command`.
526
641
  // ---------------------------------------------------------------------------
527
642
 
528
643
  function injectGeminiHooks(cwd) {
@@ -531,15 +646,20 @@ function injectGeminiHooks(cwd) {
531
646
 
532
647
  if (!data.hooks) data.hooks = {}
533
648
  data.hooks.Notification = mergeClaudeHookArray(data.hooks.Notification, 'ToolPermission', SIGNAL_CMD)
534
- // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A): skip project-scope
649
+ // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
650
+ // Wave 2 to read_file|read_many_files / write_file|replace): skip project-scope
535
651
  // credential-guard when the global one is already installed.
536
652
  const skipGeminiCG = globalCredentialGuardInstalledGemini()
537
653
  if (!skipGeminiCG) {
538
654
  data.hooks.BeforeTool = mergeClaudeHookArray(data.hooks.BeforeTool, 'run_shell_command', GUARD_CMD)
655
+ data.hooks.BeforeTool = mergeClaudeHookArray(data.hooks.BeforeTool, 'read_file|read_many_files', GUARD_CMD)
656
+ data.hooks.BeforeTool = mergeClaudeHookArray(data.hooks.BeforeTool, 'write_file|replace', GUARD_CMD)
539
657
  }
540
658
  data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, '*', CLEANUP_CMD)
541
659
  if (!skipGeminiCG) {
542
660
  data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, 'run_shell_command', GUARD_CMD)
661
+ data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, 'read_file|read_many_files', GUARD_CMD)
662
+ data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, 'write_file|replace', GUARD_CMD)
543
663
  }
544
664
 
545
665
  writeJSON(filePath, data)
@@ -586,8 +706,9 @@ function injectKiroHooks(cwd) {
586
706
  },
587
707
  ]
588
708
 
589
- // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A): skip project-scope
590
- // credential-guard entries when the global one is already installed.
709
+ // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
710
+ // Wave 2 to read/write): skip project-scope credential-guard entries when the global one is
711
+ // already installed.
591
712
  if (!globalCredentialGuardInstalledKiro()) {
592
713
  hooks.push(
593
714
  {
@@ -604,6 +725,36 @@ function injectKiroHooks(cwd) {
604
725
  matcher: 'shell',
605
726
  action: { type: 'command', command: GUARD_CMD },
606
727
  },
728
+ // Read/Write coverage (ADR-2026-08-06 emenda 7, 2026-08-08): "read" and "write" are the
729
+ // documented Kiro tool-category aliases (fs_read/fs_write), same pattern as "shell" above.
730
+ {
731
+ name: 'trackfw-credential-guard-read-pre',
732
+ description: 'Blocks/warns on possible plaintext credential materialization before a file read',
733
+ trigger: 'PreToolUse',
734
+ matcher: 'read',
735
+ action: { type: 'command', command: GUARD_CMD },
736
+ },
737
+ {
738
+ name: 'trackfw-credential-guard-read-post',
739
+ description: 'Warns on possible plaintext credential materialization after a file read',
740
+ trigger: 'PostToolUse',
741
+ matcher: 'read',
742
+ action: { type: 'command', command: GUARD_CMD },
743
+ },
744
+ {
745
+ name: 'trackfw-credential-guard-write-pre',
746
+ description: 'Blocks/warns on possible plaintext credential materialization before a file write',
747
+ trigger: 'PreToolUse',
748
+ matcher: 'write',
749
+ action: { type: 'command', command: GUARD_CMD },
750
+ },
751
+ {
752
+ name: 'trackfw-credential-guard-write-post',
753
+ description: 'Warns on possible plaintext credential materialization after a file write',
754
+ trigger: 'PostToolUse',
755
+ matcher: 'write',
756
+ action: { type: 'command', command: GUARD_CMD },
757
+ },
607
758
  )
608
759
  }
609
760
 
@@ -638,17 +789,27 @@ function injectKiroHooks(cwd) {
638
789
  // regardless.
639
790
  // ---------------------------------------------------------------------------
640
791
 
792
+ // Read/Write coverage (ADR-2026-08-06 emenda 7, ROADMAP-2026-08-08 Wave 2, 2026-08-08):
793
+ // https://docs.github.com/en/copilot/reference/hooks-reference confirms the camelCase
794
+ // preToolUse/postToolUse toolName mapping `view -> Read`, `create -> Write`, `edit -> Edit` --
795
+ // "view" is the read matcher, "create|edit" the write/edit matcher, same lowercase-runtime-name
796
+ // convention already used for "bash" above.
641
797
  function injectCopilotHooks(cwd) {
642
798
  const filePath = path.join(cwd, '.github', 'hooks', 'trackfw-attention.json')
643
799
 
644
800
  const preToolUse = [{ type: 'command', bash: SIGNAL_CMD, cwd: '.', timeoutSec: 10 }]
645
801
  const postToolUse = [{ type: 'command', bash: CLEANUP_CMD, cwd: '.', timeoutSec: 10 }]
646
802
 
647
- // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A): skip project-scope
648
- // credential-guard entries when the global one is already installed.
803
+ // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
804
+ // Wave 2 to view / create|edit): skip project-scope credential-guard entries when the global
805
+ // one is already installed.
649
806
  if (!globalCredentialGuardInstalledCopilot()) {
650
807
  preToolUse.push({ type: 'command', matcher: 'bash', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
808
+ preToolUse.push({ type: 'command', matcher: 'view', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
809
+ preToolUse.push({ type: 'command', matcher: 'create|edit', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
651
810
  postToolUse.push({ type: 'command', matcher: 'bash', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
811
+ postToolUse.push({ type: 'command', matcher: 'view', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
812
+ postToolUse.push({ type: 'command', matcher: 'create|edit', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
652
813
  }
653
814
 
654
815
  const data = {
@@ -745,7 +906,8 @@ function injectCursorHooks(cwd) {
745
906
  }
746
907
  removeKnownCommandFromLegacyTopLevelArray(data, 'postToolUse', CLEANUP_CMD)
747
908
 
748
- // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A): skip project-scope
909
+ // Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
910
+ // Wave 2 to Read/Write via the generic preToolUse/postToolUse events): skip project-scope
749
911
  // credential-guard entries when the global one is already installed.
750
912
  if (!globalCredentialGuardInstalledCursor()) {
751
913
  if (!Array.isArray(data.hooks.beforeShellExecution)) data.hooks.beforeShellExecution = []
@@ -757,6 +919,29 @@ function injectCursorHooks(cwd) {
757
919
  if (!hasEntry(data.hooks.afterShellExecution, 'command', GUARD_CMD)) {
758
920
  data.hooks.afterShellExecution.push({ command: GUARD_CMD })
759
921
  }
922
+
923
+ // Read/Write coverage (ADR-2026-08-06 emenda 7, 2026-08-08): wired via the generic
924
+ // preToolUse/postToolUse events (distinct from beforeShellExecution/afterShellExecution,
925
+ // which only ever fire for Shell) with an explicit `matcher`, so these entries never fire
926
+ // for the same tool call the unfiltered attention-signal/cleanup entries already handle
927
+ // above in this same array. hasEntry (command-only) is not enough here -- both the
928
+ // unfiltered signal entry and these matcher-scoped guard entries share the same array, so
929
+ // dedup must also check `matcher`.
930
+ const hasGuardMatcherEntry = (arr, matcher) =>
931
+ Array.isArray(arr) && arr.some(e => e && e.command === GUARD_CMD && e.matcher === matcher)
932
+
933
+ if (!hasGuardMatcherEntry(data.hooks.preToolUse, 'Read')) {
934
+ data.hooks.preToolUse.push({ command: GUARD_CMD, matcher: 'Read' })
935
+ }
936
+ if (!hasGuardMatcherEntry(data.hooks.preToolUse, 'Write')) {
937
+ data.hooks.preToolUse.push({ command: GUARD_CMD, matcher: 'Write' })
938
+ }
939
+ if (!hasGuardMatcherEntry(data.hooks.postToolUse, 'Read')) {
940
+ data.hooks.postToolUse.push({ command: GUARD_CMD, matcher: 'Read' })
941
+ }
942
+ if (!hasGuardMatcherEntry(data.hooks.postToolUse, 'Write')) {
943
+ data.hooks.postToolUse.push({ command: GUARD_CMD, matcher: 'Write' })
944
+ }
760
945
  }
761
946
 
762
947
  writeJSON(filePath, data)