trackfw 6.6.0 → 6.7.1
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 +1 -1
- package/src/generators/hooks.js +242 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.1",
|
|
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",
|
package/src/generators/hooks.js
CHANGED
|
@@ -85,6 +85,24 @@ function mergeClaudeHookArray(existing, matcher, command) {
|
|
|
85
85
|
return arr
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// migrateClaudeHookCommand rewrites a legacy hook command to a new one, in place, for every entry
|
|
89
|
+
// matching the given matcher inside a Claude PreToolUse/PostToolUse array. Used to fix
|
|
90
|
+
// .claude/settings.json files already written by an older trackfw before a command string changes
|
|
91
|
+
// -- without this, re-running `trackfw init`/`update` only ever appends the new (fixed) command
|
|
92
|
+
// alongside the stale one (merge dedup in mergeClaudeHookArray keys on the exact command string, so
|
|
93
|
+
// it can't tell "same guard, new path" from "a different hook"), leaving the broken entry in place
|
|
94
|
+
// to keep firing and failing forever.
|
|
95
|
+
function migrateClaudeHookCommand(existing, matcher, oldCommand, newCommand) {
|
|
96
|
+
const arr = Array.isArray(existing) ? existing : []
|
|
97
|
+
for (const item of arr) {
|
|
98
|
+
if (!item || item.matcher !== matcher) continue
|
|
99
|
+
const innerHooks = Array.isArray(item.hooks) ? item.hooks : []
|
|
100
|
+
for (const h of innerHooks) {
|
|
101
|
+
if (h && h.command === oldCommand) h.command = newCommand
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
// ---------------------------------------------------------------------------
|
|
89
107
|
// Scripts content
|
|
90
108
|
// ---------------------------------------------------------------------------
|
|
@@ -174,8 +192,6 @@ elif printf '%s' "$INPUT" | grep -qE "$AWS_KEY_PATTERN"; then
|
|
|
174
192
|
MATCH="AWS access key"
|
|
175
193
|
fi
|
|
176
194
|
|
|
177
|
-
[ -n "$MATCH" ] || exit 0
|
|
178
|
-
|
|
179
195
|
# The raw payload is JSON: any double quote inside the underlying tool_input.command is
|
|
180
196
|
# escaped as \\" -- unescape those before scanning for redirect targets, or a quoted target
|
|
181
197
|
# like "$TMPFILE" is seen as starting with a literal backslash instead of a variable
|
|
@@ -206,6 +222,57 @@ is_ephemeral_target() {
|
|
|
206
222
|
|
|
207
223
|
REDIRECTS=$(printf '%s' "$RAW" | grep -oE '[0-9]?>>?[[:space:]]*[^[:space:]|&;,:]+' || true)
|
|
208
224
|
|
|
225
|
+
# Second detection layer: only runs when the payload scan above found nothing -- keeps the common
|
|
226
|
+
# case (match already found) cheap and avoids reading files unnecessarily. Files above the size cap
|
|
227
|
+
# are skipped silently.
|
|
228
|
+
scan_file_for_pattern() {
|
|
229
|
+
local path size
|
|
230
|
+
path=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
|
|
231
|
+
[ -n "$path" ] && [ -f "$path" ] || return 1
|
|
232
|
+
size=$(wc -c < "$path" 2>/dev/null | tr -d '[:space:]')
|
|
233
|
+
size=\${size:-0}
|
|
234
|
+
[ "$size" -lt 1048576 ] || return 1
|
|
235
|
+
if grep -qE "$JWT_PATTERN" "$path" 2>/dev/null; then
|
|
236
|
+
MATCH="JWT"
|
|
237
|
+
return 0
|
|
238
|
+
fi
|
|
239
|
+
if grep -qE "$AWS_KEY_PATTERN" "$path" 2>/dev/null; then
|
|
240
|
+
MATCH="AWS access key"
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
return 1
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if [ -z "$MATCH" ] && [ -n "$REDIRECTS" ]; then
|
|
247
|
+
while IFS= read -r line; do
|
|
248
|
+
if [ -z "$line" ]; then
|
|
249
|
+
continue
|
|
250
|
+
fi
|
|
251
|
+
target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
|
|
252
|
+
if ! is_ephemeral_target "$target"; then
|
|
253
|
+
scan_file_for_pattern "$target" && break
|
|
254
|
+
fi
|
|
255
|
+
done <<< "$REDIRECTS"
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
if [ -z "$MATCH" ]; then
|
|
259
|
+
CMD_LINE=$(printf '%s' "$RAW" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
|
|
260
|
+
if [ -n "$CMD_LINE" ]; then
|
|
261
|
+
set -- $CMD_LINE
|
|
262
|
+
cmd_name="\${1:-}"
|
|
263
|
+
case "$cmd_name" in
|
|
264
|
+
cat|head|tail|jq|grep)
|
|
265
|
+
shift
|
|
266
|
+
for token in "$@"; do
|
|
267
|
+
scan_file_for_pattern "$token" && break
|
|
268
|
+
done
|
|
269
|
+
;;
|
|
270
|
+
esac
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
[ -n "$MATCH" ] || exit 0
|
|
275
|
+
|
|
209
276
|
HAS_REDIRECT=0
|
|
210
277
|
EXEMPT=1
|
|
211
278
|
if [ -n "$REDIRECTS" ]; then
|
|
@@ -227,10 +294,22 @@ fi
|
|
|
227
294
|
|
|
228
295
|
`
|
|
229
296
|
|
|
230
|
-
|
|
297
|
+
// Resolução de MODE (grep de `credential_guard.mode` em trackfw.yaml + fallback) é inlined,
|
|
298
|
+
// idêntica em CG_PROJECT_TAIL (fallback "warn") e CG_GLOBAL_TAIL (fallback "block") — ao invés de
|
|
299
|
+
// extrair para uma constante JS compartilhada e concatenar (como o Go faz via
|
|
300
|
+
// credentialGuardModeResolution/DEFAULT_MODE), aqui o texto é replicado como literal em cada
|
|
301
|
+
// template literal: o gate de paridade Go/Node/Python
|
|
302
|
+
// (internal/generators/credential_guard_test.go, getNodeSourceBlock) extrai cada constante via
|
|
303
|
+
// regex de um único bloco `` `const NAME = \`...\`` `` sem suportar concatenação de string —
|
|
304
|
+
// concatenar quebraria a extração estática. Nunca editar a lógica de resolução em só um dos dois
|
|
305
|
+
// blocos sem replicar no outro (ML-1B, ADR-2026-08-06 emenda 6). Mirrors Go's
|
|
306
|
+
// credentialGuardModeResolution (semântica idêntica, forma sintática diferente por essa restrição
|
|
307
|
+
// do parser de paridade).
|
|
308
|
+
const CG_PROJECT_TAIL = `DEFAULT_MODE="warn"
|
|
309
|
+
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
310
|
case "$MODE" in
|
|
232
311
|
warn|block) ;;
|
|
233
|
-
*) MODE="
|
|
312
|
+
*) MODE="$DEFAULT_MODE" ;;
|
|
234
313
|
esac
|
|
235
314
|
|
|
236
315
|
if [ "$MODE" = "block" ]; then
|
|
@@ -259,10 +338,42 @@ printf '{"tool":"credential-guard","message":"%s","level":"action_required","tim
|
|
|
259
338
|
exit 0
|
|
260
339
|
`
|
|
261
340
|
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
341
|
+
// CG_GLOBAL_TAIL é a contraparte de CG_PROJECT_TAIL para o escopo global
|
|
342
|
+
// (~/.trackfw/scripts/trackfw-credential-guard.sh, instalado via `trackfw update harness`).
|
|
343
|
+
//
|
|
344
|
+
// Decisão (ML-1B, ver ADR-2026-08-06 emenda 6 de 2026-08-08 e ROADMAP-2026-08-08, Wave 1): o modo
|
|
345
|
+
// em escopo global reusa a MESMA leitura de `credential_guard.mode` de trackfw.yaml que
|
|
346
|
+
// CG_PROJECT_TAIL já faz (mesma resolução, replicada aqui — ver o comentário de CG_PROJECT_TAIL
|
|
347
|
+
// sobre por que não é extraída para uma constante compartilhada em Node) — sem exigir trackfw.yaml
|
|
348
|
+
// existir (não há o guard `[ -f trackfw.yaml ] || exit 0` da variante de projeto: o objetivo do
|
|
349
|
+
// escopo global é proteger qualquer projeto, com ou sem trackfw.yaml). Quando o hook global roda a
|
|
350
|
+
// partir do cwd de um projeto com trackfw.yaml e credential_guard.mode explícito, esse valor é
|
|
351
|
+
// respeitado (warn ou block) — nenhuma mudança de comportamento para quem já definiu mode: warn
|
|
352
|
+
// explicitamente. Em qualquer outro caso (sem trackfw.yaml, ou trackfw.yaml sem essa chave), o
|
|
353
|
+
// fallback deixa de ser "warn" e passa a ser "block": um guard opt-in que nunca bloqueia por
|
|
354
|
+
// padrão é uma falsa sensação de proteção — o usuário que rodou `trackfw update harness` já
|
|
355
|
+
// demonstrou intenção explícita de ter o mecanismo ativo. Supersede a decisão original ("modo
|
|
356
|
+
// global sempre warn", opção "b" avaliada na ADR original) — não cria ~/.trackfw/config.yaml nem
|
|
357
|
+
// nenhuma outra segunda fonte de configuração só para isto.
|
|
358
|
+
//
|
|
359
|
+
// ROADMAP_DIR em escopo global: como não há garantia de trackfw.yaml para ler `roadmap_dir:`, o
|
|
360
|
+
// script usa o caminho padrão fixo "docs/roadmaps" relativo ao cwd de onde o hook foi disparado, e
|
|
361
|
+
// só grava o attention signal se esse diretório já existir (e só em modo warn — modo block nunca
|
|
362
|
+
// grava o attention signal, mesma decisão da variante de projeto). Não cria "docs/roadmaps" em um
|
|
363
|
+
// projeto aleatório só para sinalizar isso — isso pareceria ao usuário que o trackfw foi
|
|
364
|
+
// "instalado" nesse projeto, o que não é verdade. O texto de warning/block em stderr acontece
|
|
365
|
+
// sempre (visível no output do CLI/hook), independente de o diretório de attention existir.
|
|
366
|
+
const CG_GLOBAL_TAIL = `DEFAULT_MODE="block"
|
|
367
|
+
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)
|
|
368
|
+
case "$MODE" in
|
|
369
|
+
warn|block) ;;
|
|
370
|
+
*) MODE="$DEFAULT_MODE" ;;
|
|
371
|
+
esac
|
|
372
|
+
|
|
373
|
+
if [ "$MODE" = "block" ]; then
|
|
374
|
+
echo "trackfw-credential-guard: blocked - possible $MATCH detected in tool payload." >&2
|
|
375
|
+
exit 2
|
|
376
|
+
fi
|
|
266
377
|
|
|
267
378
|
echo "trackfw-credential-guard: warning - possible $MATCH detected in tool payload." >&2
|
|
268
379
|
|
|
@@ -326,6 +437,16 @@ function generateGlobalCredentialGuardScript(home) {
|
|
|
326
437
|
const SIGNAL_CMD = 'scripts/trackfw-attention-signal.sh'
|
|
327
438
|
const CLEANUP_CMD = 'scripts/trackfw-attention-cleanup.sh'
|
|
328
439
|
const GUARD_CMD = 'scripts/trackfw-credential-guard.sh'
|
|
440
|
+
// Claude Code only (2026-08-09 fix, reported in production against the CMDB project): Claude Code
|
|
441
|
+
// resolves a bare relative hook command against the hook's *dynamic* cwd (tracks `cd`s the agent
|
|
442
|
+
// runs during the session), not the project root -- confirmed against
|
|
443
|
+
// https://code.claude.com/docs/en/hooks: "Handlers run in the current directory... cwd is dynamic".
|
|
444
|
+
// Any Bash/Read/Write/Edit call after the agent `cd`s into a subdirectory (e.g. a monorepo package)
|
|
445
|
+
// made the hook fail with "No such file or directory". $CLAUDE_PROJECT_DIR is the env var Claude
|
|
446
|
+
// Code guarantees stays pinned to the project root regardless of cwd drift (same doc) -- used here
|
|
447
|
+
// instead of GUARD_CMD, matching the pattern this project's own custom hooks already relied on
|
|
448
|
+
// successfully in practice.
|
|
449
|
+
const GUARD_CMD_CLAUDE = '$CLAUDE_PROJECT_DIR/scripts/trackfw-credential-guard.sh'
|
|
329
450
|
|
|
330
451
|
// ---------------------------------------------------------------------------
|
|
331
452
|
// Global credential-guard dedup (ROADMAP-2026-08-06 Wave 3/ML-3A)
|
|
@@ -459,14 +580,30 @@ function injectClaudeHooks(cwd) {
|
|
|
459
580
|
|
|
460
581
|
if (!data.hooks) data.hooks = {}
|
|
461
582
|
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'AskUserQuestion', SIGNAL_CMD)
|
|
462
|
-
|
|
463
|
-
// credential-guard
|
|
583
|
+
|
|
584
|
+
// Rewrite any stale relative-path credential-guard command from an older trackfw run before
|
|
585
|
+
// merging the fixed one below, so upgrading doesn't just append a second, still-broken entry
|
|
586
|
+
// alongside the new one (see GUARD_CMD_CLAUDE comment for the "No such file or directory" bug).
|
|
587
|
+
for (const matcher of ['Bash', 'Read', 'Write|Edit']) {
|
|
588
|
+
migrateClaudeHookCommand(data.hooks.PreToolUse, matcher, GUARD_CMD, GUARD_CMD_CLAUDE)
|
|
589
|
+
migrateClaudeHookCommand(data.hooks.PostToolUse, matcher, GUARD_CMD, GUARD_CMD_CLAUDE)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
|
|
593
|
+
// Wave 2 to Read/Write|Edit): skip project-scope credential-guard when the global one is
|
|
594
|
+
// already installed for this CLI.
|
|
464
595
|
if (!globalCredentialGuardInstalledClaude()) {
|
|
465
|
-
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Bash',
|
|
596
|
+
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Bash', GUARD_CMD_CLAUDE)
|
|
597
|
+
// ADR-2026-08-06 emenda 7 (2026-08-08): Read/Write/Edit coverage — extraction via direct
|
|
598
|
+
// file read, or materialization via write/edit, never went through the hook before.
|
|
599
|
+
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Read', GUARD_CMD_CLAUDE)
|
|
600
|
+
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Write|Edit', GUARD_CMD_CLAUDE)
|
|
466
601
|
}
|
|
467
602
|
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'AskUserQuestion', CLEANUP_CMD)
|
|
468
603
|
if (!globalCredentialGuardInstalledClaude()) {
|
|
469
|
-
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Bash',
|
|
604
|
+
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Bash', GUARD_CMD_CLAUDE)
|
|
605
|
+
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Read', GUARD_CMD_CLAUDE)
|
|
606
|
+
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Write|Edit', GUARD_CMD_CLAUDE)
|
|
470
607
|
}
|
|
471
608
|
|
|
472
609
|
writeJSON(filePath, data)
|
|
@@ -483,6 +620,12 @@ function injectClaudeHooks(cwd) {
|
|
|
483
620
|
// are enabled by default (no `[features] hooks = true`/`codex_hooks` opt-in
|
|
484
621
|
// needed -- that flag exists only to turn hooks OFF), and PreToolUse blocking
|
|
485
622
|
// uses exit code 2 + stderr (matching trackfw-credential-guard.sh's "block" mode).
|
|
623
|
+
//
|
|
624
|
+
// Read/Write/Edit coverage (ADR-2026-08-06 emenda 7, ROADMAP-2026-08-08 Wave 2, 2026-08-08):
|
|
625
|
+
// Codex has NO dedicated, interceptable read-tool matcher -- confirmed against
|
|
626
|
+
// https://learn.chatgpt.com/docs/hooks -- so no read matcher is added here; this is a documented
|
|
627
|
+
// limitation (also called out in docs/cli-parity.md), not a workaround. Write/edit materialization
|
|
628
|
+
// IS covered via the `apply_patch` matcher (documented aliases `Edit`/`Write`).
|
|
486
629
|
// ---------------------------------------------------------------------------
|
|
487
630
|
|
|
488
631
|
function injectCodexHooks(cwd) {
|
|
@@ -491,15 +634,18 @@ function injectCodexHooks(cwd) {
|
|
|
491
634
|
|
|
492
635
|
if (!data.hooks) data.hooks = {}
|
|
493
636
|
data.hooks.PermissionRequest = mergeClaudeHookArray(data.hooks.PermissionRequest, '.*', SIGNAL_CMD)
|
|
494
|
-
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A
|
|
495
|
-
// credential-guard when the global one is already
|
|
637
|
+
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
|
|
638
|
+
// Wave 2 to apply_patch): skip project-scope credential-guard when the global one is already
|
|
639
|
+
// installed for this CLI.
|
|
496
640
|
const skipCodexCG = globalCredentialGuardInstalledCodex()
|
|
497
641
|
if (!skipCodexCG) {
|
|
498
642
|
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Bash', GUARD_CMD)
|
|
643
|
+
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'apply_patch', GUARD_CMD)
|
|
499
644
|
}
|
|
500
645
|
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, '.*', CLEANUP_CMD)
|
|
501
646
|
if (!skipCodexCG) {
|
|
502
647
|
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Bash', GUARD_CMD)
|
|
648
|
+
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'apply_patch', GUARD_CMD)
|
|
503
649
|
}
|
|
504
650
|
|
|
505
651
|
writeJSON(filePath, data)
|
|
@@ -523,6 +669,12 @@ function injectCodexHooks(cwd) {
|
|
|
523
669
|
// (matcher "*") is the pre-existing attention-cleanup wiring, unrelated to the new
|
|
524
670
|
// credential-guard entry added as a separate array entry (different matcher) in the
|
|
525
671
|
// same event.
|
|
672
|
+
//
|
|
673
|
+
// Read/Write/Edit coverage (ADR-2026-08-06 emenda 7, ROADMAP-2026-08-08 Wave 2, 2026-08-08): the
|
|
674
|
+
// Gemini CLI tools table (https://geminicli.com/docs/reference/tools) documents `read_file`/
|
|
675
|
+
// `read_many_files` as the file-read tools and `write_file`/`replace` as the file-write/edit
|
|
676
|
+
// tools -- matcher below follows the same regex-over-tool_name convention already used for
|
|
677
|
+
// `run_shell_command`.
|
|
526
678
|
// ---------------------------------------------------------------------------
|
|
527
679
|
|
|
528
680
|
function injectGeminiHooks(cwd) {
|
|
@@ -531,15 +683,20 @@ function injectGeminiHooks(cwd) {
|
|
|
531
683
|
|
|
532
684
|
if (!data.hooks) data.hooks = {}
|
|
533
685
|
data.hooks.Notification = mergeClaudeHookArray(data.hooks.Notification, 'ToolPermission', SIGNAL_CMD)
|
|
534
|
-
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A
|
|
686
|
+
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
|
|
687
|
+
// Wave 2 to read_file|read_many_files / write_file|replace): skip project-scope
|
|
535
688
|
// credential-guard when the global one is already installed.
|
|
536
689
|
const skipGeminiCG = globalCredentialGuardInstalledGemini()
|
|
537
690
|
if (!skipGeminiCG) {
|
|
538
691
|
data.hooks.BeforeTool = mergeClaudeHookArray(data.hooks.BeforeTool, 'run_shell_command', GUARD_CMD)
|
|
692
|
+
data.hooks.BeforeTool = mergeClaudeHookArray(data.hooks.BeforeTool, 'read_file|read_many_files', GUARD_CMD)
|
|
693
|
+
data.hooks.BeforeTool = mergeClaudeHookArray(data.hooks.BeforeTool, 'write_file|replace', GUARD_CMD)
|
|
539
694
|
}
|
|
540
695
|
data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, '*', CLEANUP_CMD)
|
|
541
696
|
if (!skipGeminiCG) {
|
|
542
697
|
data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, 'run_shell_command', GUARD_CMD)
|
|
698
|
+
data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, 'read_file|read_many_files', GUARD_CMD)
|
|
699
|
+
data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, 'write_file|replace', GUARD_CMD)
|
|
543
700
|
}
|
|
544
701
|
|
|
545
702
|
writeJSON(filePath, data)
|
|
@@ -586,8 +743,9 @@ function injectKiroHooks(cwd) {
|
|
|
586
743
|
},
|
|
587
744
|
]
|
|
588
745
|
|
|
589
|
-
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A
|
|
590
|
-
// credential-guard entries when the global one is
|
|
746
|
+
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
|
|
747
|
+
// Wave 2 to read/write): skip project-scope credential-guard entries when the global one is
|
|
748
|
+
// already installed.
|
|
591
749
|
if (!globalCredentialGuardInstalledKiro()) {
|
|
592
750
|
hooks.push(
|
|
593
751
|
{
|
|
@@ -604,6 +762,36 @@ function injectKiroHooks(cwd) {
|
|
|
604
762
|
matcher: 'shell',
|
|
605
763
|
action: { type: 'command', command: GUARD_CMD },
|
|
606
764
|
},
|
|
765
|
+
// Read/Write coverage (ADR-2026-08-06 emenda 7, 2026-08-08): "read" and "write" are the
|
|
766
|
+
// documented Kiro tool-category aliases (fs_read/fs_write), same pattern as "shell" above.
|
|
767
|
+
{
|
|
768
|
+
name: 'trackfw-credential-guard-read-pre',
|
|
769
|
+
description: 'Blocks/warns on possible plaintext credential materialization before a file read',
|
|
770
|
+
trigger: 'PreToolUse',
|
|
771
|
+
matcher: 'read',
|
|
772
|
+
action: { type: 'command', command: GUARD_CMD },
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: 'trackfw-credential-guard-read-post',
|
|
776
|
+
description: 'Warns on possible plaintext credential materialization after a file read',
|
|
777
|
+
trigger: 'PostToolUse',
|
|
778
|
+
matcher: 'read',
|
|
779
|
+
action: { type: 'command', command: GUARD_CMD },
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
name: 'trackfw-credential-guard-write-pre',
|
|
783
|
+
description: 'Blocks/warns on possible plaintext credential materialization before a file write',
|
|
784
|
+
trigger: 'PreToolUse',
|
|
785
|
+
matcher: 'write',
|
|
786
|
+
action: { type: 'command', command: GUARD_CMD },
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
name: 'trackfw-credential-guard-write-post',
|
|
790
|
+
description: 'Warns on possible plaintext credential materialization after a file write',
|
|
791
|
+
trigger: 'PostToolUse',
|
|
792
|
+
matcher: 'write',
|
|
793
|
+
action: { type: 'command', command: GUARD_CMD },
|
|
794
|
+
},
|
|
607
795
|
)
|
|
608
796
|
}
|
|
609
797
|
|
|
@@ -638,17 +826,27 @@ function injectKiroHooks(cwd) {
|
|
|
638
826
|
// regardless.
|
|
639
827
|
// ---------------------------------------------------------------------------
|
|
640
828
|
|
|
829
|
+
// Read/Write coverage (ADR-2026-08-06 emenda 7, ROADMAP-2026-08-08 Wave 2, 2026-08-08):
|
|
830
|
+
// https://docs.github.com/en/copilot/reference/hooks-reference confirms the camelCase
|
|
831
|
+
// preToolUse/postToolUse toolName mapping `view -> Read`, `create -> Write`, `edit -> Edit` --
|
|
832
|
+
// "view" is the read matcher, "create|edit" the write/edit matcher, same lowercase-runtime-name
|
|
833
|
+
// convention already used for "bash" above.
|
|
641
834
|
function injectCopilotHooks(cwd) {
|
|
642
835
|
const filePath = path.join(cwd, '.github', 'hooks', 'trackfw-attention.json')
|
|
643
836
|
|
|
644
837
|
const preToolUse = [{ type: 'command', bash: SIGNAL_CMD, cwd: '.', timeoutSec: 10 }]
|
|
645
838
|
const postToolUse = [{ type: 'command', bash: CLEANUP_CMD, cwd: '.', timeoutSec: 10 }]
|
|
646
839
|
|
|
647
|
-
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A
|
|
648
|
-
// credential-guard entries when the global
|
|
840
|
+
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
|
|
841
|
+
// Wave 2 to view / create|edit): skip project-scope credential-guard entries when the global
|
|
842
|
+
// one is already installed.
|
|
649
843
|
if (!globalCredentialGuardInstalledCopilot()) {
|
|
650
844
|
preToolUse.push({ type: 'command', matcher: 'bash', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
|
|
845
|
+
preToolUse.push({ type: 'command', matcher: 'view', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
|
|
846
|
+
preToolUse.push({ type: 'command', matcher: 'create|edit', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
|
|
651
847
|
postToolUse.push({ type: 'command', matcher: 'bash', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
|
|
848
|
+
postToolUse.push({ type: 'command', matcher: 'view', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
|
|
849
|
+
postToolUse.push({ type: 'command', matcher: 'create|edit', bash: GUARD_CMD, cwd: '.', timeoutSec: 10 })
|
|
652
850
|
}
|
|
653
851
|
|
|
654
852
|
const data = {
|
|
@@ -745,7 +943,8 @@ function injectCursorHooks(cwd) {
|
|
|
745
943
|
}
|
|
746
944
|
removeKnownCommandFromLegacyTopLevelArray(data, 'postToolUse', CLEANUP_CMD)
|
|
747
945
|
|
|
748
|
-
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A
|
|
946
|
+
// Dedup (ROADMAP-2026-08-06 Wave 3/ML-3A, extended ADR-2026-08-06 emenda 7/ROADMAP-2026-08-08
|
|
947
|
+
// Wave 2 to Read/Write via the generic preToolUse/postToolUse events): skip project-scope
|
|
749
948
|
// credential-guard entries when the global one is already installed.
|
|
750
949
|
if (!globalCredentialGuardInstalledCursor()) {
|
|
751
950
|
if (!Array.isArray(data.hooks.beforeShellExecution)) data.hooks.beforeShellExecution = []
|
|
@@ -757,6 +956,29 @@ function injectCursorHooks(cwd) {
|
|
|
757
956
|
if (!hasEntry(data.hooks.afterShellExecution, 'command', GUARD_CMD)) {
|
|
758
957
|
data.hooks.afterShellExecution.push({ command: GUARD_CMD })
|
|
759
958
|
}
|
|
959
|
+
|
|
960
|
+
// Read/Write coverage (ADR-2026-08-06 emenda 7, 2026-08-08): wired via the generic
|
|
961
|
+
// preToolUse/postToolUse events (distinct from beforeShellExecution/afterShellExecution,
|
|
962
|
+
// which only ever fire for Shell) with an explicit `matcher`, so these entries never fire
|
|
963
|
+
// for the same tool call the unfiltered attention-signal/cleanup entries already handle
|
|
964
|
+
// above in this same array. hasEntry (command-only) is not enough here -- both the
|
|
965
|
+
// unfiltered signal entry and these matcher-scoped guard entries share the same array, so
|
|
966
|
+
// dedup must also check `matcher`.
|
|
967
|
+
const hasGuardMatcherEntry = (arr, matcher) =>
|
|
968
|
+
Array.isArray(arr) && arr.some(e => e && e.command === GUARD_CMD && e.matcher === matcher)
|
|
969
|
+
|
|
970
|
+
if (!hasGuardMatcherEntry(data.hooks.preToolUse, 'Read')) {
|
|
971
|
+
data.hooks.preToolUse.push({ command: GUARD_CMD, matcher: 'Read' })
|
|
972
|
+
}
|
|
973
|
+
if (!hasGuardMatcherEntry(data.hooks.preToolUse, 'Write')) {
|
|
974
|
+
data.hooks.preToolUse.push({ command: GUARD_CMD, matcher: 'Write' })
|
|
975
|
+
}
|
|
976
|
+
if (!hasGuardMatcherEntry(data.hooks.postToolUse, 'Read')) {
|
|
977
|
+
data.hooks.postToolUse.push({ command: GUARD_CMD, matcher: 'Read' })
|
|
978
|
+
}
|
|
979
|
+
if (!hasGuardMatcherEntry(data.hooks.postToolUse, 'Write')) {
|
|
980
|
+
data.hooks.postToolUse.push({ command: GUARD_CMD, matcher: 'Write' })
|
|
981
|
+
}
|
|
760
982
|
}
|
|
761
983
|
|
|
762
984
|
writeJSON(filePath, data)
|