Skip to content

feat: deterministic enforcement hooks for migration quality#33

Open
AlexDeMichieli wants to merge 1 commit into
mainfrom
feat/plugin-hooks
Open

feat: deterministic enforcement hooks for migration quality#33
AlexDeMichieli wants to merge 1 commit into
mainfrom
feat/plugin-hooks

Conversation

@AlexDeMichieli
Copy link
Copy Markdown
Collaborator

@AlexDeMichieli AlexDeMichieli commented Jun 3, 2026

What this brings

This PR adds 5 hooks to the CLI plugin that enforce migration quality deterministically — the agent cannot bypass these checks.

Hooks overview

Hook Event Trigger Behavior
Secret detection preToolUse create|edit (matcher) Hard-denies file writes with hardcoded secrets. Forces ${{ secrets.NAME }}.
File deletion guard preToolUse bash (matcher) Hard-denies rm outside .github/ci-archive/. Prevents source code deletion.
Quality check + actionlint postToolUse create|edit (matcher) After each workflow write, injects warnings into agent context: unpinned actions, placeholders, write-all permissions, missing permissions, actionlint errors. Agent sees these on the same turn.
Quality gate agentStop every turn Scans ALL workflow files when agent finishes a turn. Blocks completion (decision: "block") if any workflows have issues, forcing the agent to fix them. 3-attempt safety valve prevents infinite loops.
Migration scorecard sessionEnd session end Appends an entry to .github/MIGRATION-SCORECARD.md — audit artifact with session ID, timestamp, completion reason, and workflow quality counts (total/clean/issues). Each session appends rather than overwrites, so you see progression across multiple passes.

How enforcement works

Agent writes workflow → postToolUse injects quality warnings (same turn)
Agent finishes turn   → agentStop blocks if issues remain (forces another turn)
Agent blocked 3x      → safety valve releases
Session ends          → sessionEnd appends scorecard entry

actionlint runs in 3 hooks: postToolUse (per-file immediate), agentStop (all-files gate), sessionEnd (final counts). The agent cannot skip linting.

What is actionlint and how do hooks help?

actionlint is a static analysis tool for GitHub Actions workflow files. It catches syntax errors, invalid triggers, unpinned actions, shellcheck issues, bad permissions, incorrect needs: references, and more. Running it after every migration is critical — without it, generated workflows may look correct but fail at runtime.

Before hooks, the agent had to: (1) load the actionlint skill into context (~4.5k tokens), (2) install actionlint via shell commands, (3) run it manually, (4) read the output, (5) fix issues — each step a separate agent turn consuming ~100k+ input tokens. The agent could also skip any of these steps since skills are advisory.

With hooks, this entire cycle is handled deterministically by three hooks:

  • postToolUse (matcher: create|edit): Fires immediately after the agent writes a workflow file. Auto-installs actionlint if missing, runs it on the file, and injects errors as additionalContext on the same turn — the agent sees issues without spending extra turns.
  • agentStop: Fires when the agent finishes a turn. Scans all workflow files with actionlint. If any have errors, returns decision: "block" forcing the agent to fix them before completing. The agent cannot finish with broken workflows.
  • sessionEnd: Fires at session close. Runs actionlint on all workflows for the final clean/issues count in the migration scorecard.

The hooks cannot be skipped or ignored — they're shell commands that execute outside the model. This is the difference between "please run actionlint" (instruction the agent can ignore) and "actionlint will run and block you" (hook).

actionlint auto-install

All 3 hooks that use actionlint will install it automatically if not present:

  • Linux (cloud agent): downloads pinned v1.7.11 binary from GitHub releases (checksum-verified upstream)
  • macOS (CLI): brew install actionlint
  • Already installed: skips with zero overhead

Timeouts are set to 60s/60s/45s to accommodate the one-time install. This eliminates the dependency on the agent remembering to invoke the actionlint skill for installation — the hook handles it deterministically.

Migration scorecard

The sessionEnd hook appends to .github/MIGRATION-SCORECARD.md after every session. Multiple passes show quality progression:

# Migration Scorecard

## 2026-06-03T15:48:35Z
- Session: 9404dcaf-13f7-4d29-a2a3-6611f5ac281b
- Reason: complete
- Workflows: 3 total, 0 clean, 3 with issues

## 2026-06-03T15:49:29Z
- Session: a3089be4-ce5e-47ae-947d-6b5c52cc0f01
- Reason: complete
- Workflows: 3 total, 1 clean, 2 with issues

Each workflow is checked for:

  • Unpinned actions — version tags (@v4) instead of SHA refs
  • Placeholder text — TODO, FIXME, CHANGEME, PLACEHOLDER, XXX
  • Over-broad permissionspermissions: write-all
  • actionlint errors — syntax/semantic issues caught by the linter

A workflow must pass all four checks to count as "clean."

Correct hooks API

Uses the hooks reference API correctly:

  • permissionDecision: "deny" / permissionDecisionReason for preToolUse blocks
  • matcher regex filtering on preToolUse and postToolUse (replaces shell-level tool name checks)
  • agentStop with decision: "block" / reason for forced continuation
  • sessionEnd for audit artifacts
  • stdin JSON payloads with toolName, toolArgs, cwd, sessionId

Testing

  • Unit tested: 21 cases covering all 5 hooks — secret allow/deny, rm guard, quality detection, gate blocking with safety-valve, scorecard generation and append behavior
  • Live tested: Two-pass Copilot CLI 1.0.58 session with --log-level debug — confirmed all 5 hook types fire, agentStop returns decision: "block", sessionEnd appends scorecard entries with real session IDs, and the second pass shows quality improvement
  • Auto-install verified: All 3 actionlint hooks contain Linux (pinned binary) and macOS (brew) install paths with bumped timeouts

Cloud agent compatibility

All 5 hooks use events supported by Copilot cloud agent. agentStop decision: "block" forces another turn (counts against job timeout). sessionEnd fires once per job. Only bash command hooks are used (no PowerShell). actionlint auto-installs from GitHub releases on Linux.

Token savings

Without hooks, a typical actionlint cycle looks like this:

Turn What happens Input tokens
1 Agent decides to invoke the actionlint skill ~100k
2 Agent reads skill, runs install command, reads output ~105k
3 Agent runs actionlint, reads lint output, starts fixing ~105k
4 Agent finishes fixing, moves on ~105k

That's 3 extra turns (~315k input tokens) just for linting — repeated for every workflow file.

With hooks, the same cycle is:

Turn What happens Input tokens
1 Agent writes workflow. Hook auto-installs actionlint (if needed), runs it, injects results as additionalContext on the same turn. Agent sees errors immediately. ~100k
2 Agent fixes errors. Hook re-runs actionlint, confirms clean. ~100k

That's ~100-200k saved per workflow file — no skill invocation, no manual install, no separate lint run. At scale across dozens of migrations with multiple workflow files each, this adds up.

Note: The actionlint skill is intentionally kept in this PR. Hooks and the skill are complementary, not redundant:

  • CLI / Cloud agent: Hooks fire automatically (install + run + inject results). The skill may still be loaded by the agent since platform skills and agents reference it — this is intentional to preserve CCA compatibility.
  • CCA / VS Code: Does not support hooks. The skill and its references across platform skills and agents are the only linting mechanism.

Follow-up: Once CCA supports hooks, remove plugin/skills/actionlint/SKILL.md and strip the "pair with actionlint" references from all 7 platform skills, migration-core, and all agent files. This will eliminate the redundant skill invocation on CLI/cloud and fully realize the token savings described above.

Files changed

  • plugin/hooks.json — 5 production hooks with auto-install, matcher filtering, correct API fields, and append scorecard
  • plugin/README.md — updated hooks documentation

@AlexDeMichieli AlexDeMichieli force-pushed the feat/plugin-hooks branch 4 times, most recently from c99e748 to e18c746 Compare June 3, 2026 15:58
@AlexDeMichieli AlexDeMichieli marked this pull request as ready for review June 4, 2026 20:50
@AlexDeMichieli AlexDeMichieli requested a review from antgrutta as a code owner June 4, 2026 20:50
Copilot AI review requested due to automatic review settings June 4, 2026 20:50
@AlexDeMichieli AlexDeMichieli requested a review from ssulei7 as a code owner June 4, 2026 20:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a plugin/hooks.json hook pack for the Copilot CLI plugin to deterministically enforce migration-quality constraints (block/deny unsafe tool calls, inject workflow quality feedback, and produce an audit scorecard), and documents the new enforcement model in plugin/README.md.

Changes:

  • Adds deterministic enforcement hooks (preToolUse, postToolUse, agentStop, sessionEnd) in plugin/hooks.json.
  • Implements workflow “quality” detection (unpinned actions, placeholders, write-all, missing permissions, actionlint) and a blocking quality gate with a 3-attempt safety valve.
  • Documents hook behavior, rationale, and how to enable/disable hooks in plugin/README.md.
Show a summary per file
File Description
plugin/hooks.json Adds 5 hooks to block hardcoded secrets and unsafe deletions, lint/check workflows, enforce a blocking quality gate, and append a migration scorecard.
plugin/README.md Documents the hooks, their lifecycle events, and how to verify/disable them.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 7

Comment thread plugin/hooks.json Outdated
"description": "Block hardcoded secrets in file writes",
"matcher": "create|edit",
"timeoutSec": 10,
"bash": "INPUT=$(cat); ARGS=$(echo \"$INPUT\" | jq -c '.toolArgs' 2>/dev/null); HAS_SECRET=$(echo \"$ARGS\" | grep -ciE '(password|secret|token|api[_-]?key)\\s*[:=]' 2>/dev/null || true); HAS_EXPR=$(echo \"$ARGS\" | grep -cF '${' 2>/dev/null || true); if [ \"${HAS_SECRET:-0}\" -gt 0 ] && [ \"${HAS_EXPR:-0}\" -eq 0 ]; then echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked: hardcoded secret detected. Use GitHub Secrets (${{ secrets.NAME }}) instead.\"}'; else echo '{}'; fi"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Secret detection now: (1) extracts only the content/new_string field from toolArgs, (2) checks each line individually — a line must match a secret keyword + [:=] + 8+ chars of value AND not contain \${ on that same line, (3) secrets: inherit no longer triggers because inherit is <8 chars and there's no [:=] value pattern.

Comment thread plugin/hooks.json Outdated
"description": "Guard against file deletion outside ci-archive",
"matcher": "bash",
"timeoutSec": 10,
"bash": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.toolArgs.command // .toolArgs // empty' 2>/dev/null); if echo \"$CMD\" | grep -qE 'rm\\s+(-[rfi]+\\s+)*' 2>/dev/null; then if ! echo \"$CMD\" | grep -q '.github/ci-archive' 2>/dev/null; then echo '{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Blocked: file deletion only allowed inside .github/ci-archive/. Move source CI files there before removing.\"}'; exit 0; fi; fi; echo '{}'"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The rm guard now: (1) checks for ... path traversal BEFORE checking ci-archive paths — any target containing .. is denied immediately, (2) uses a case statement with glob patterns (*/.github/ci-archive/* and *.github/ci-archive/*) for literal matching instead of regex, so xgithub/ci-archive no longer slips through.

Comment thread plugin/hooks.json Outdated
"description": "Quality check and actionlint on workflow files after write",
"matcher": "create|edit",
"timeoutSec": 60,
"bash": "INPUT=$(cat); ARGS=$(echo \"$INPUT\" | jq -c '.toolArgs' 2>/dev/null); FILE=$(echo \"$ARGS\" | jq -r '.file_path // .path // empty' 2>/dev/null); echo \"$FILE\" | grep -q '.github/workflows/' || exit 0; [ -f \"$FILE\" ] || exit 0; if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; W=''; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$FILE\" 2>/dev/null && W=\"${W}- Unpinned actions: use full SHA commit refs\\n\"; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$FILE\" 2>/dev/null && W=\"${W}- Placeholder text found: replace before merging\\n\"; grep -qE 'permissions:\\s*write-all' \"$FILE\" 2>/dev/null && W=\"${W}- Over-broad permissions: replace write-all with least-privilege\\n\"; grep -qE '^permissions:' \"$FILE\" 2>/dev/null || W=\"${W}- Missing top-level permissions block\\n\"; L=''; command -v actionlint >/dev/null 2>&1 && L=$(actionlint \"$FILE\" 2>&1 | head -5); if [ -n \"$W\" ] || [ -n \"$L\" ]; then MSG=\"MIGRATION QUALITY CHECK ($FILE):\\n${W}\"; [ -n \"$L\" ] && MSG=\"${MSG}actionlint errors:\\n${L}\\n\"; MSG=\"${MSG}Fix these issues now.\"; ESCAPED=$(printf '%s' \"$MSG\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); printf '{\"additionalContext\":\"%s\"}' \"$ESCAPED\"; fi"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. All 3 hooks now download to a temp file and verify SHA256 (900919a8...) before extracting. Checksum mismatch aborts install and surfaces an explicit warning. Also fixed the PR description — removed the 'checksum-verified upstream' claim and replaced with accurate description.

Comment thread plugin/hooks.json Outdated
"type": "command",
"description": "Migration quality gate — block completion if workflows have issues",
"timeoutSec": 60,
"bash": "INPUT=$(cat); CWD=$(echo \"$INPUT\" | jq -r '.cwd // \".\"' 2>/dev/null); COUNTER_FILE='/tmp/.migration-quality-gate'; COUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo 0); COUNT=$((COUNT + 1)); echo \"$COUNT\" > \"$COUNTER_FILE\"; if [ \"$COUNT\" -gt 3 ]; then rm -f \"$COUNTER_FILE\"; echo '{}'; exit 0; fi; if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; ISSUES=''; for f in \"$CWD\"/.github/workflows/*.yml \"$CWD\"/.github/workflows/*.yaml; do [ -f \"$f\" ] || continue; FN=$(basename \"$f\"); W=''; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$f\" 2>/dev/null && W=\"${W}unpinned-actions \"; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$f\" 2>/dev/null && W=\"${W}placeholders \"; grep -qE 'permissions:\\s*write-all' \"$f\" 2>/dev/null && W=\"${W}write-all \"; command -v actionlint >/dev/null 2>&1 && ! actionlint \"$f\" >/dev/null 2>&1 && W=\"${W}actionlint-errors \"; [ -n \"$W\" ] && ISSUES=\"${ISSUES}${FN}: ${W}; \"; done; if [ -n \"$ISSUES\" ]; then ESCAPED=$(printf '%s' \"$ISSUES\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); printf '{\"decision\":\"block\",\"reason\":\"Migration quality gate FAILED (attempt %d/3). Fix these workflow issues:\\n%s\"}' \"$COUNT\" \"$ESCAPED\"; else rm -f \"$COUNTER_FILE\"; echo '{}'; fi"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. agentStop now includes grep -qE '^permissions:' || W='no-permissions ' in its per-file scan, consistent with postToolUse.

Comment thread plugin/hooks.json Outdated
"type": "command",
"description": "Generate final migration scorecard",
"timeoutSec": 45,
"bash": "INPUT=$(cat); CWD=$(echo \"$INPUT\" | jq -r '.cwd // \".\"' 2>/dev/null); REASON=$(echo \"$INPUT\" | jq -r '.reason // \"unknown\"' 2>/dev/null); SESSION=$(echo \"$INPUT\" | jq -r '.sessionId // \"unknown\"' 2>/dev/null); if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; TOTAL=0; CLEAN=0; BAD=0; for f in \"$CWD\"/.github/workflows/*.yml \"$CWD\"/.github/workflows/*.yaml; do [ -f \"$f\" ] || continue; TOTAL=$((TOTAL + 1)); HAS=0; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$f\" 2>/dev/null && HAS=1; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$f\" 2>/dev/null && HAS=1; grep -qE 'permissions:\\s*write-all' \"$f\" 2>/dev/null && HAS=1; command -v actionlint >/dev/null 2>&1 && ! actionlint \"$f\" >/dev/null 2>&1 && HAS=1; [ \"$HAS\" -eq 0 ] && CLEAN=$((CLEAN + 1)) || BAD=$((BAD + 1)); done; rm -f /tmp/.migration-quality-gate; SC=\"$CWD/.github/MIGRATION-SCORECARD.md\"; [ -f \"$SC\" ] || printf '# Migration Scorecard\\n' > \"$SC\" 2>/dev/null; printf '\\n## %s\\n- Session: %s\\n- Reason: %s\\n- Workflows: %d total, %d clean, %d with issues\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$SESSION\" \"$REASON\" \"$TOTAL\" \"$CLEAN\" \"$BAD\" >> \"$SC\" 2>/dev/null; echo '{}'"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. sessionEnd scorecard now includes grep -qE '^permissions:' || HAS=1 so missing-permissions counts as an issue. Consistent with all other hooks.

Comment thread plugin/README.md Outdated
| File deletion guard | `preToolUse` | `bash` | Hard-denies `rm` operations outside `.github/ci-archive/`. Prevents accidental deletion of application source code. |
| Quality check + actionlint | `postToolUse` | `create\|edit` | After any workflow file write, injects `additionalContext` with: unpinned actions (tag vs SHA), placeholder text (TODO/FIXME), over-broad permissions (`write-all`), missing permissions block, and actionlint errors. The agent sees these on the same turn. |
| **Quality gate** | `agentStop` | — | Scans ALL workflow files when the agent finishes a turn. If any have issues, returns `decision: "block"` forcing the agent to take another turn to fix them. Safety valve releases after 3 attempts to prevent infinite loops. |
| **Migration scorecard** | `sessionEnd` | — | Generates `.github/MIGRATION-SCORECARD.md` with session ID, timestamp, completion reason, and workflow counts (total / clean / with-issues). Audit artifact for migration quality tracking. |
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Description changed to 'Appends an entry to' and hook description changed to 'Append migration scorecard entry'.

Comment thread plugin/hooks.json Outdated
"description": "Quality check and actionlint on workflow files after write",
"matcher": "create|edit",
"timeoutSec": 60,
"bash": "INPUT=$(cat); ARGS=$(echo \"$INPUT\" | jq -c '.toolArgs' 2>/dev/null); FILE=$(echo \"$ARGS\" | jq -r '.file_path // .path // empty' 2>/dev/null); echo \"$FILE\" | grep -q '.github/workflows/' || exit 0; [ -f \"$FILE\" ] || exit 0; if ! command -v actionlint >/dev/null 2>&1; then if [ \"$(uname)\" = 'Linux' ]; then V='1.7.11'; curl -fsSL \"https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz\" | tar xz -C /tmp actionlint 2>/dev/null && install -m 755 /tmp/actionlint /usr/local/bin/actionlint 2>/dev/null; elif command -v brew >/dev/null 2>&1; then brew install actionlint 2>/dev/null; fi; fi; W=''; grep -qE 'uses:\\s+[^@]+@v[0-9]' \"$FILE\" 2>/dev/null && W=\"${W}- Unpinned actions: use full SHA commit refs\\n\"; grep -qiE '(TODO|FIXME|CHANGEME|PLACEHOLDER|XXX)' \"$FILE\" 2>/dev/null && W=\"${W}- Placeholder text found: replace before merging\\n\"; grep -qE 'permissions:\\s*write-all' \"$FILE\" 2>/dev/null && W=\"${W}- Over-broad permissions: replace write-all with least-privilege\\n\"; grep -qE '^permissions:' \"$FILE\" 2>/dev/null || W=\"${W}- Missing top-level permissions block\\n\"; L=''; command -v actionlint >/dev/null 2>&1 && L=$(actionlint \"$FILE\" 2>&1 | head -5); if [ -n \"$W\" ] || [ -n \"$L\" ]; then MSG=\"MIGRATION QUALITY CHECK ($FILE):\\n${W}\"; [ -n \"$L\" ] && MSG=\"${MSG}actionlint errors:\\n${L}\\n\"; MSG=\"${MSG}Fix these issues now.\"; ESCAPED=$(printf '%s' \"$MSG\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); printf '{\"additionalContext\":\"%s\"}' \"$ESCAPED\"; fi"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. All 3 hooks now: (1) on checksum mismatch, abort install and inject explicit warning via additionalContext, (2) after install attempt, re-check command -v actionlint — if still missing, inject 'actionlint not available (install failed)' warning, (3) agentStop adds 'actionlint-unavailable' to the issues list so it blocks completion.

… actionlint

Rebuild hooks.json with correct Copilot hooks API:
- preToolUse (matcher: create|edit): secret detection with permissionDecision deny
- preToolUse (matcher: bash): rm guard blocks deletion outside ci-archive
- postToolUse (matcher: create|edit): quality check + actionlint per workflow file
- agentStop: quality gate blocks agent completion until workflows pass (3-attempt safety valve)
- sessionEnd: generates MIGRATION-SCORECARD.md with session stats

Key changes from previous version:
- Use permissionDecision/permissionDecisionReason (not decision/reason)
- Add matcher filtering (no more shell-level tool name checks)
- agentStop replaces postToolUse-only approach — actually blocks completion
- sessionEnd provides audit artifact for migration quality tracking
- actionlint runs in 3 hooks: postToolUse, agentStop, sessionEnd
- Test harness with 21 passing tests included
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants