@danmoisan/drm-copilot-mcp 1.0.27 → 1.1.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.
Files changed (34) hide show
  1. package/out/mcp-server.js +0 -1
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
  4. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
  5. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
  6. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
  7. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +64 -20
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
  12. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
  13. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
  14. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +36 -24
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
  21. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
  22. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
  23. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
  25. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
  26. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
  27. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
  28. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
  29. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
  30. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +63 -0
  31. package/resources/claude-customizations/config/blast-radius.json +9 -3
  32. package/resources/claude-customizations/pack-manifests/core.json +3 -0
  33. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  34. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +15 -0
package/out/mcp-server.js CHANGED
@@ -19004,7 +19004,6 @@ var EXCLUDED_DIR_NAMES = /* @__PURE__ */ new Set([
19004
19004
  ]);
19005
19005
  var SCAN_DEPTH_LIMIT = 3;
19006
19006
  var PAYLOAD_MODULES = {
19007
- "claude-runtime": [".claude/**"],
19008
19007
  config: ["config/**"]
19009
19008
  };
19010
19009
  var FORBIDDEN_GLOBS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.0.27",
3
+ "version": "1.1.0",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -5,10 +5,10 @@
5
5
  .DESCRIPTION
6
6
  This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
7
7
  operation on a file path matching a Pester test file (*.Tests.ps1 or tests/**/*.ps1).
8
- It reads the tool input from the CLAUDE_TOOL_INPUT environment variable (JSON with
9
- 'file_path' and a content field: 'content' for Write, 'new_string' for Edit) and
10
- rejects the operation when the proposed content introduces forbidden runtime
11
- dependencies or violates the mocking rules.
8
+ It acquires the hook payload through the shared reader and reads the envelope's
9
+ nested tool_input ('file_path' plus a content field: 'content' for Write,
10
+ 'new_string' for Edit), then rejects the operation when the proposed content
11
+ introduces forbidden runtime dependencies or violates the mocking rules.
12
12
 
13
13
  Forbidden patterns in Pester unit tests include:
14
14
  - direct external-executable mocking (Mock git, Mock gh, Mock actionlint, etc.)
@@ -32,6 +32,8 @@
32
32
  [CmdletBinding()]
33
33
  param()
34
34
 
35
+
36
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
35
37
  function Get-PowerShellTestPurityBlockDecision {
36
38
  [CmdletBinding()]
37
39
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -68,18 +70,15 @@ function Invoke-PowerShellTestPurityDecision {
68
70
  [string] $ToolInputRaw
69
71
  )
70
72
 
71
- if (-not $ToolInputRaw) {
72
- return $null
73
- }
74
-
75
- try {
76
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
77
- }
78
- catch {
79
- return $null
73
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
74
+ if (-not $payload.IsValid) {
75
+ return Get-PowerShellTestPurityBlockDecision -Reason (
76
+ 'PowerShell unit test purity hook received an unreadable PreToolUse envelope: ' +
77
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
78
+ '. The gate fails closed on an envelope it cannot read.')
80
79
  }
81
80
 
82
- $filePath = $toolInput.file_path
81
+ $filePath = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'file_path'
83
82
  if (-not $filePath) {
84
83
  return $null
85
84
  }
@@ -88,12 +87,9 @@ function Invoke-PowerShellTestPurityDecision {
88
87
  return $null
89
88
  }
90
89
 
91
- $content = $null
92
- if ($null -ne $toolInput.content) {
93
- $content = [string]$toolInput.content
94
- }
95
- elseif ($null -ne $toolInput.new_string) {
96
- $content = [string]$toolInput.new_string
90
+ $content = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'content'
91
+ if (-not $content) {
92
+ $content = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'new_string'
97
93
  }
98
94
 
99
95
  if (-not $content) {
@@ -141,7 +137,7 @@ if ($MyInvocation.InvocationName -eq '.') {
141
137
  return
142
138
  }
143
139
 
144
- $decision = Invoke-PowerShellTestPurityDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
140
+ $decision = Invoke-PowerShellTestPurityDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
145
141
  if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {
146
142
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
147
143
  }
@@ -4,10 +4,10 @@
4
4
 
5
5
  .DESCRIPTION
6
6
  This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
7
- operation on a file path matching tests/**/*.py. It reads the tool input from the
8
- CLAUDE_TOOL_INPUT environment variable (JSON with 'file_path' and a content field:
9
- 'content' for Write, 'new_string' for Edit) and rejects the operation when the
10
- proposed content introduces forbidden runtime dependencies.
7
+ operation on a file path matching tests/**/*.py. It acquires the hook payload
8
+ through the shared reader and reads the envelope's nested tool_input ('file_path'
9
+ plus a content field: 'content' for Write, 'new_string' for Edit), then rejects the
10
+ operation when the proposed content introduces forbidden runtime dependencies.
11
11
 
12
12
  Forbidden patterns in unit tests include:
13
13
  - temporary filesystem usage (tempfile, NamedTemporaryFile, TemporaryDirectory,
@@ -29,6 +29,8 @@
29
29
  [CmdletBinding()]
30
30
  param()
31
31
 
32
+
33
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
32
34
  function Get-PythonTestPurityBlockDecision {
33
35
  [CmdletBinding()]
34
36
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -65,17 +67,15 @@ function Invoke-PythonTestPurityDecision {
65
67
  [string] $ToolInputRaw
66
68
  )
67
69
 
68
- if (-not $ToolInputRaw) {
69
- return $null
70
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
71
+ if (-not $payload.IsValid) {
72
+ return Get-PythonTestPurityBlockDecision -Reason (
73
+ 'Python unit test purity hook received an unreadable PreToolUse envelope: ' +
74
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
75
+ '. The gate fails closed on an envelope it cannot read.')
70
76
  }
71
77
 
72
- try {
73
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
74
- } catch {
75
- return Get-PythonTestPurityBlockDecision -Reason 'Python unit test purity hook received malformed JSON in CLAUDE_TOOL_INPUT.'
76
- }
77
-
78
- $filePath = $toolInput.file_path
78
+ $filePath = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'file_path'
79
79
  if (-not $filePath) {
80
80
  return $null
81
81
  }
@@ -84,11 +84,9 @@ function Invoke-PythonTestPurityDecision {
84
84
  return $null
85
85
  }
86
86
 
87
- $content = $null
88
- if ($null -ne $toolInput.content) {
89
- $content = [string]$toolInput.content
90
- } elseif ($null -ne $toolInput.new_string) {
91
- $content = [string]$toolInput.new_string
87
+ $content = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'content'
88
+ if (-not $content) {
89
+ $content = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'new_string'
92
90
  }
93
91
 
94
92
  if (-not $content) {
@@ -141,7 +139,7 @@ if ($MyInvocation.InvocationName -eq '.') {
141
139
  return
142
140
  }
143
141
 
144
- $decision = Invoke-PythonTestPurityDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
142
+ $decision = Invoke-PythonTestPurityDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
145
143
  if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {
146
144
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
147
145
  }
@@ -43,6 +43,8 @@
43
43
  [CmdletBinding()]
44
44
  param()
45
45
 
46
+
47
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
46
48
  $script:CanonicalStepPrefixes = @(
47
49
  'S0_startup_checks',
48
50
  'S1_change_budget_estimation',
@@ -197,7 +199,7 @@ function Test-IsCheckpointPath {
197
199
  function Invoke-CheckpointMonotonicDecision {
198
200
  <#
199
201
  .SYNOPSIS
200
- Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
202
+ Parses the PreToolUse envelope and returns an allow-or-block decision.
201
203
  #>
202
204
  [CmdletBinding()]
203
205
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -205,18 +207,20 @@ function Invoke-CheckpointMonotonicDecision {
205
207
  [string] $ToolInputRaw
206
208
  )
207
209
 
208
- if (-not $ToolInputRaw) {
209
- return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
210
- }
211
-
212
- try {
213
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
214
- }
215
- catch {
216
- throw "enforce-checkpoint-monotonic hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
210
+ $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
211
+ if (-not $envelope.IsValid) {
212
+ return [ordered]@{
213
+ hookSpecificOutput = [ordered]@{
214
+ hookEventName = 'PreToolUse'
215
+ permissionDecision = 'deny'
216
+ permissionDecisionReason = 'CHECKPOINT_MONOTONIC_BLOCKED: payload anomaly - ' +
217
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
218
+ '. The gate fails closed on an envelope it cannot read.'
219
+ }
220
+ }
217
221
  }
218
222
 
219
- $filePath = $toolInput.file_path
223
+ $filePath = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'file_path'
220
224
  if (-not $filePath) {
221
225
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
222
226
  }
@@ -228,7 +232,7 @@ function Invoke-CheckpointMonotonicDecision {
228
232
 
229
233
  # Write tool: validate the content payload. Edit tool: partial new_string is
230
234
  # not reliable without the full target file content, so allow.
231
- $content = $toolInput.content
235
+ $content = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'content'
232
236
  if (-not $content) {
233
237
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
234
238
  }
@@ -298,13 +302,7 @@ if ($MyInvocation.InvocationName -eq '.') {
298
302
  return
299
303
  }
300
304
 
301
- try {
302
- $decision = Invoke-CheckpointMonotonicDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
303
- }
304
- catch {
305
- Write-Error $_
306
- exit 1
307
- }
305
+ $decision = Invoke-CheckpointMonotonicDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
308
306
 
309
307
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
310
308
 
@@ -41,6 +41,8 @@
41
41
  [CmdletBinding()]
42
42
  param()
43
43
 
44
+
45
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
44
46
  # Dot-source the shared validation helpers. Guarded so a missing file produces a
45
47
  # clear error and so dot-sourcing this hook in tests loads the helpers too.
46
48
  $script:CompletionHelpersPath = Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1'
@@ -314,7 +316,7 @@ function Resolve-EditedCheckpointContent {
314
316
  function Invoke-CompletionConsistencyDecision {
315
317
  <#
316
318
  .SYNOPSIS
317
- Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision based on
319
+ Parses the PreToolUse envelope and returns an allow-or-block decision based on
318
320
  completion-evidence consistency.
319
321
  #>
320
322
  [CmdletBinding()]
@@ -332,18 +334,21 @@ function Invoke-CompletionConsistencyDecision {
332
334
  [scriptblock] $RoutingMatrixReader
333
335
  )
334
336
 
335
- if (-not $ToolInputRaw) {
336
- return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
337
- }
338
-
339
- try {
340
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
341
- }
342
- catch {
343
- throw "enforce-completion-consistency hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
337
+ $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
338
+ if (-not $envelope.IsValid) {
339
+ return [ordered]@{
340
+ hookSpecificOutput = [ordered]@{
341
+ hookEventName = 'PreToolUse'
342
+ permissionDecision = 'deny'
343
+ permissionDecisionReason = 'COMPLETION_CONSISTENCY_BLOCKED: payload anomaly - ' +
344
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
345
+ '. The gate fails closed on an envelope it cannot read.'
346
+ }
347
+ }
344
348
  }
345
349
 
346
- $filePath = $toolInput.file_path
350
+ $toolInput = $envelope.Value
351
+ $filePath = Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'file_path'
347
352
  if (-not $filePath) {
348
353
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
349
354
  }
@@ -356,7 +361,7 @@ function Invoke-CompletionConsistencyDecision {
356
361
  # Write tool: validate the content payload directly. Edit tool: no content is
357
362
  # supplied, so read the on-disk checkpoint through the injectable seam and
358
363
  # apply the old_string -> new_string patch in memory (read-then-validate).
359
- $content = $toolInput.content
364
+ $content = Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'content'
360
365
  if (-not $content) {
361
366
  $content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader
362
367
  if (-not $content) {
@@ -403,13 +408,7 @@ if ($MyInvocation.InvocationName -eq '.') {
403
408
  return
404
409
  }
405
410
 
406
- try {
407
- $decision = Invoke-CompletionConsistencyDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
408
- }
409
- catch {
410
- Write-Error $_
411
- exit 1
412
- }
411
+ $decision = Invoke-CompletionConsistencyDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
413
412
 
414
413
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
415
414
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  .DESCRIPTION
7
7
  Invoked by the Claude Code PreToolUse hook on Write or Edit operations
8
- (matcher "Write|Edit"). The hook reads $env:CLAUDE_TOOL_INPUT JSON
8
+ (matcher "Write|Edit"). The hook reads the PreToolUse envelope JSON
9
9
  containing file_path, and either content (Write) or old_string/new_string
10
10
  (Edit).
11
11
 
@@ -33,6 +33,8 @@
33
33
  [CmdletBinding()]
34
34
  param()
35
35
 
36
+
37
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
36
38
  function Invoke-DiscoveryValidatorExe {
37
39
  <#
38
40
  .SYNOPSIS
@@ -151,7 +153,7 @@ function Get-RequiredDiscoveryArtifactDeclaration {
151
153
  function Invoke-DiscoveryArtifactGateDecision {
152
154
  <#
153
155
  .SYNOPSIS
154
- Parses CLAUDE_TOOL_INPUT and returns an allow-or-deny decision for a
156
+ Parses the PreToolUse envelope and returns an allow-or-deny decision for a
155
157
  discovery-artifact completion gate.
156
158
  #>
157
159
  [CmdletBinding()]
@@ -163,18 +165,21 @@ function Invoke-DiscoveryArtifactGateDecision {
163
165
  [scriptblock] $RequiredArtifactReader = { Get-RequiredDiscoveryArtifactDeclaration }
164
166
  )
165
167
 
166
- if (-not $ToolInputRaw) {
167
- return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
168
- }
169
-
170
- try {
171
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
172
- }
173
- catch {
174
- throw "enforce-discovery-artifact-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
168
+ $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
169
+ if (-not $envelope.IsValid) {
170
+ return [ordered]@{
171
+ hookSpecificOutput = [ordered]@{
172
+ hookEventName = 'PreToolUse'
173
+ permissionDecision = 'deny'
174
+ permissionDecisionReason = 'DISCOVERY_ARTIFACT_GATE_BLOCKED: payload anomaly - ' +
175
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
176
+ '. The gate fails closed on an envelope it cannot read.'
177
+ }
178
+ }
175
179
  }
176
180
 
177
- $filePath = $toolInput.file_path
181
+ $toolInput = $envelope.Value
182
+ $filePath = Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'file_path'
178
183
  if (-not $filePath) {
179
184
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
180
185
  }
@@ -220,13 +225,7 @@ if ($MyInvocation.InvocationName -eq '.') {
220
225
  return
221
226
  }
222
227
 
223
- try {
224
- $decision = Invoke-DiscoveryArtifactGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
225
- }
226
- catch {
227
- Write-Error $_
228
- exit 1
229
- }
228
+ $decision = Invoke-DiscoveryArtifactGateDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
230
229
 
231
230
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
232
231
 
@@ -10,16 +10,22 @@
10
10
  subagent_type is 'epic-planner', 'epic-orchestrator', 'parallel-planner',
11
11
  or 'parallel-orchestrator'.
12
12
 
13
+ The payload is acquired through the shared reader
14
+ (.claude/lib/hook-payload/HookPayload.psm1: stdin first, then the two
15
+ environment-variable fallbacks).
16
+
13
17
  Caller identity resolution:
14
- - The full PreToolUse payload (CLAUDE_HOOK_INPUT) carries a top-level
15
- 'agent_type' field only when the tool call is made from inside a
16
- subagent context. A main-thread call carries no 'agent_type'.
17
- - The Agent tool input (CLAUDE_TOOL_INPUT, or the payload's 'tool_input'
18
- object) carries the delegation target 'subagent_type'.
18
+ - The PreToolUse envelope carries a top-level 'agent_type' field only when
19
+ the tool call is made from inside a subagent context. A main-thread call
20
+ carries no 'agent_type'.
21
+ - The envelope's nested 'tool_input' object carries the delegation target
22
+ 'subagent_type'.
19
23
 
20
24
  Decision procedure:
21
- 1. Resolve the target subagent_type from CLAUDE_TOOL_INPUT, falling back
22
- to the payload's tool_input object. A non-gated target allows.
25
+ 0. An envelope this hook cannot read at all -- empty on every transport,
26
+ unparseable, or carrying no usable tool_input -- denies (fail closed).
27
+ 1. Resolve the target subagent_type from the nested tool_input. A non-gated
28
+ target allows.
23
29
  2. Resolve the calling agent_type from the payload. An absent or empty
24
30
  agent_type indicates a main-thread invocation, which allows.
25
31
  3. Deny when the calling agent_type is exactly 'orchestrator', with the
@@ -37,9 +43,12 @@
37
43
  [CmdletBinding()]
38
44
  param()
39
45
 
46
+
47
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
40
48
  $script:GatedSubagentTypes = @('epic-planner', 'epic-orchestrator', 'parallel-planner', 'parallel-orchestrator')
41
49
  $script:ParallelSubagentTypes = @('parallel-planner', 'parallel-orchestrator')
42
50
  $script:ProhibitedCallerAgentType = 'orchestrator'
51
+ $script:EmptyPayloadAnomaly = 'EmptyPayload'
43
52
 
44
53
  function Get-EpicInvocationOriginAllowDecision {
45
54
  [CmdletBinding()]
@@ -110,9 +119,9 @@ function Get-EpicInvocationOriginTargetSubagent {
110
119
  Resolves the delegation target subagent_type from the tool input,
111
120
  falling back to the full payload's tool_input object.
112
121
  .PARAMETER ToolInput
113
- Parsed CLAUDE_TOOL_INPUT object, or $null when absent.
122
+ Parsed legacy tool-input object, or $null when absent.
114
123
  .PARAMETER HookInput
115
- Parsed CLAUDE_HOOK_INPUT object, or $null when absent.
124
+ Parsed PreToolUse envelope object, or $null when absent.
116
125
  .OUTPUTS
117
126
  System.String or $null
118
127
  #>
@@ -151,7 +160,7 @@ function Get-EpicInvocationOriginCallerAgentType {
151
160
  Resolves the calling agent_type from the full hook payload. Returns
152
161
  $null for a main-thread invocation (no agent_type field).
153
162
  .PARAMETER HookInput
154
- Parsed CLAUDE_HOOK_INPUT object, or $null when absent.
163
+ Parsed PreToolUse envelope object, or $null when absent.
155
164
  .OUTPUTS
156
165
  System.String or $null
157
166
  #>
@@ -180,10 +189,17 @@ function Invoke-EpicInvocationOriginDecision {
180
189
  <#
181
190
  .SYNOPSIS
182
191
  Parses the hook payloads and returns an allow-or-block decision.
192
+ .DESCRIPTION
193
+ The caller's agent_type is read off the envelope root and the delegation
194
+ target's subagent_type off the nested tool_input. An envelope this hook cannot
195
+ read at all -- empty on every transport, unparseable, or carrying no usable
196
+ tool_input -- is a fail-closed deny, because an unreadable envelope means the
197
+ PreToolUse contract drifted (issue #501).
183
198
  .PARAMETER HookInputRaw
184
- The raw full PreToolUse payload JSON supplied via CLAUDE_HOOK_INPUT.
199
+ The raw PreToolUse envelope JSON acquired by Read-ClaudeHookRawPayload.
185
200
  .PARAMETER ToolInputRaw
186
- The raw Agent tool input JSON supplied via CLAUDE_TOOL_INPUT.
201
+ Optional legacy direct tool-input payload, retained so an undocumented wrapper
202
+ that supplies the bare tool input keeps working.
187
203
  .OUTPUTS
188
204
  System.Collections.Specialized.OrderedDictionary
189
205
  #>
@@ -199,29 +215,40 @@ function Invoke-EpicInvocationOriginDecision {
199
215
  [string] $ToolInputRaw
200
216
  )
201
217
 
202
- # The tool input identifies the delegation target; a non-gated target is
203
- # outside this hook's scope, so the hook input is not parsed for it.
204
- $toolInput = ConvertFrom-EpicInvocationOriginPayload -RawPayload $ToolInputRaw -PayloadName 'CLAUDE_TOOL_INPUT'
205
- $hookInputParsed = $false
218
+ # The envelope carries both halves: the caller's agent_type at the root and the
219
+ # delegation target's subagent_type inside tool_input.
220
+ $anomaly = $script:EmptyPayloadAnomaly
206
221
  $hookInput = $null
222
+ if (-not [string]::IsNullOrWhiteSpace($HookInputRaw)) {
223
+ $resolved = Resolve-ClaudeHookToolInput -Raw $HookInputRaw
224
+ $hookInput = $resolved.Envelope
225
+ $anomaly = if ($resolved.IsValid) { $null } else { $resolved.Anomaly }
226
+ }
227
+
228
+ # Legacy direct tool-input payload, retained for an undocumented wrapper that
229
+ # supplies the bare tool input rather than the documented envelope.
230
+ $toolInput = $null
231
+ if (-not [string]::IsNullOrWhiteSpace($ToolInputRaw)) {
232
+ $legacy = ConvertFrom-ClaudeHookEnvelope -Raw $ToolInputRaw
233
+ if ($legacy.IsValid) {
234
+ $toolInput = $legacy.Value
235
+ $anomaly = $null
236
+ }
237
+ }
207
238
 
208
239
  $target = Get-EpicInvocationOriginTargetSubagent -ToolInput $toolInput -HookInput $hookInput
209
- if (-not $target -and -not [string]::IsNullOrWhiteSpace($HookInputRaw)) {
210
- # Fallback: some harness surfaces supply only the full payload, whose
211
- # tool_input object carries the target subagent_type.
212
- $hookInput = ConvertFrom-EpicInvocationOriginPayload -RawPayload $HookInputRaw -PayloadName 'CLAUDE_HOOK_INPUT'
213
- $hookInputParsed = $true
214
- $target = Get-EpicInvocationOriginTargetSubagent -ToolInput $toolInput -HookInput $hookInput
240
+
241
+ if (-not $target -and $anomaly) {
242
+ return Get-EpicInvocationOriginBlockDecision -Reason (
243
+ 'EPIC_INVOCATION_ORIGIN_BLOCKED: payload anomaly - ' +
244
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $anomaly) +
245
+ '. The gate fails closed on an envelope it cannot read.')
215
246
  }
216
247
 
217
248
  if (-not $target -or $script:GatedSubagentTypes -notcontains $target) {
218
249
  return Get-EpicInvocationOriginAllowDecision
219
250
  }
220
251
 
221
- if (-not $hookInputParsed) {
222
- $hookInput = ConvertFrom-EpicInvocationOriginPayload -RawPayload $HookInputRaw -PayloadName 'CLAUDE_HOOK_INPUT'
223
- }
224
-
225
252
  # An absent agent_type marks a main-thread invocation, which is the
226
253
  # intended entry point for every gated agent; only an orchestrator-context
227
254
  # invocation is prohibited.
@@ -246,12 +273,7 @@ if ($MyInvocation.InvocationName -eq '.') {
246
273
  return
247
274
  }
248
275
 
249
- try {
250
- $decision = Invoke-EpicInvocationOriginDecision -HookInputRaw $env:CLAUDE_HOOK_INPUT -ToolInputRaw $env:CLAUDE_TOOL_INPUT
251
- } catch {
252
- Write-Error $_
253
- exit 1
254
- }
276
+ $decision = Invoke-EpicInvocationOriginDecision -HookInputRaw (Read-ClaudeHookRawPayload)
255
277
 
256
278
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
257
279