@danmoisan/drm-copilot-mcp 1.0.26 → 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.
- package/out/mcp-server.js +542 -42
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/settings.json +5 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
- package/resources/claude-customizations/config/blast-radius.json +16 -2
- package/resources/claude-customizations/pack-manifests/core.json +22 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +31 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Shared PreToolUse hook payload reader: transport, envelope parse, and strict
|
|
4
|
+
nested tool_input extraction (issue #501).
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Every PreToolUse hook under .claude/hooks/ acquires its payload through this
|
|
8
|
+
module instead of re-implementing the parse inline. The module owns three
|
|
9
|
+
concerns.
|
|
10
|
+
|
|
11
|
+
Transport. Claude Code delivers command-hook input on stdin as JSON.
|
|
12
|
+
Read-ClaudeHookRawPayload reads stdin first, then falls back to the
|
|
13
|
+
CLAUDE_HOOK_INPUT environment variable and then the CLAUDE_TOOL_INPUT
|
|
14
|
+
environment variable; the first non-whitespace source wins. Both stdin
|
|
15
|
+
operations sit behind injectable scriptblock seams so tests drive them without
|
|
16
|
+
touching a .NET static and without spawning a process.
|
|
17
|
+
|
|
18
|
+
Shape. Tool arguments are nested under the envelope's tool_input object.
|
|
19
|
+
Get-ClaudeHookToolInput extracts that object strictly: a parsed payload with no
|
|
20
|
+
tool_input key, or a tool_input that is null or not an object, is an envelope
|
|
21
|
+
anomaly. There is deliberately no flat-root fallback, because a permissive
|
|
22
|
+
dual-shape reader would keep every legacy-shaped fixture green and restore the
|
|
23
|
+
silent-allow failure mode on the next contract drift.
|
|
24
|
+
|
|
25
|
+
Anomaly classification. The parse and extract functions never return a silent
|
|
26
|
+
null for malformed input; they return a typed result whose Anomaly property
|
|
27
|
+
names the failure, and Get-ClaudeHookPayloadAnomalyReason maps that code to the
|
|
28
|
+
deny reason text a hook emits.
|
|
29
|
+
|
|
30
|
+
Fail-closed posture: an envelope-level anomaly is a deny, emitted as decision
|
|
31
|
+
JSON at process exit code 0. Exit code 1 is non-blocking for PreToolUse, so a
|
|
32
|
+
throwing hook is itself a fail-open. Property-level absence inside a well-formed
|
|
33
|
+
tool_input is NOT an anomaly: that is each hook's own scope filter and keeps its
|
|
34
|
+
existing allow early-return.
|
|
35
|
+
|
|
36
|
+
.NOTES
|
|
37
|
+
Compatible with PowerShell 7+. No external module dependencies, no filesystem
|
|
38
|
+
access, no subprocess, no network, and no wall-clock read. Mirrored
|
|
39
|
+
byte-identically under extensions/drm-copilot/resources/claude-customizations/.
|
|
40
|
+
#>
|
|
41
|
+
|
|
42
|
+
Set-StrictMode -Version Latest
|
|
43
|
+
|
|
44
|
+
# Anomaly codes. These are the only values Get-ClaudeHookPayloadAnomalyReason maps.
|
|
45
|
+
$script:AnomalyEmptyPayload = 'EmptyPayload'
|
|
46
|
+
$script:AnomalyUnparseableJson = 'UnparseableJson'
|
|
47
|
+
$script:AnomalyMissingToolInput = 'MissingToolInput'
|
|
48
|
+
$script:AnomalyNullToolInput = 'NullToolInput'
|
|
49
|
+
$script:AnomalyNonObjectToolInput = 'NonObjectToolInput'
|
|
50
|
+
|
|
51
|
+
# The nested key that carries the tool arguments in the documented envelope.
|
|
52
|
+
$script:ToolInputKey = 'tool_input'
|
|
53
|
+
|
|
54
|
+
# Deny reason text per anomaly code. Each string is distinct so an operator can
|
|
55
|
+
# tell which leg of the contract drifted from the emitted decision alone.
|
|
56
|
+
$script:AnomalyReasonText = @{
|
|
57
|
+
'EmptyPayload' = 'the hook received an empty payload on stdin and on both environment-variable fallbacks'
|
|
58
|
+
'UnparseableJson' = 'the hook received a payload that is not parseable JSON'
|
|
59
|
+
'MissingToolInput' = 'the hook received a JSON payload with no tool_input key (the legacy flat root shape is an envelope anomaly, not a supported payload)'
|
|
60
|
+
'NullToolInput' = 'the hook received a JSON payload whose tool_input is null'
|
|
61
|
+
'NonObjectToolInput' = 'the hook received a JSON payload whose tool_input is not an object'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Get-ClaudeHookPayloadAnomalyCode {
|
|
65
|
+
<#
|
|
66
|
+
.SYNOPSIS
|
|
67
|
+
Return the anomaly codes this module can emit, so callers need not
|
|
68
|
+
hard-code the literals in a second place.
|
|
69
|
+
.OUTPUTS
|
|
70
|
+
System.String[]
|
|
71
|
+
#>
|
|
72
|
+
[CmdletBinding()]
|
|
73
|
+
[OutputType([string[]])]
|
|
74
|
+
param()
|
|
75
|
+
|
|
76
|
+
return [string[]]@(
|
|
77
|
+
$script:AnomalyEmptyPayload,
|
|
78
|
+
$script:AnomalyUnparseableJson,
|
|
79
|
+
$script:AnomalyMissingToolInput,
|
|
80
|
+
$script:AnomalyNullToolInput,
|
|
81
|
+
$script:AnomalyNonObjectToolInput
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Get-ClaudeHookPayloadAnomalyReason {
|
|
86
|
+
<#
|
|
87
|
+
.SYNOPSIS
|
|
88
|
+
Map an anomaly code to the clause a hook puts in its deny reason.
|
|
89
|
+
.PARAMETER Anomaly
|
|
90
|
+
One of the codes returned by Get-ClaudeHookPayloadAnomalyCode.
|
|
91
|
+
.OUTPUTS
|
|
92
|
+
System.String
|
|
93
|
+
#>
|
|
94
|
+
[CmdletBinding()]
|
|
95
|
+
[OutputType([string])]
|
|
96
|
+
param(
|
|
97
|
+
[AllowNull()]
|
|
98
|
+
[AllowEmptyString()]
|
|
99
|
+
[string] $Anomaly
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if ([string]::IsNullOrWhiteSpace($Anomaly)) {
|
|
103
|
+
return 'the hook received an unclassified payload anomaly'
|
|
104
|
+
}
|
|
105
|
+
if ($script:AnomalyReasonText.ContainsKey($Anomaly)) {
|
|
106
|
+
return [string]$script:AnomalyReasonText[$Anomaly]
|
|
107
|
+
}
|
|
108
|
+
return ('the hook received an unrecognized payload anomaly ({0})' -f $Anomaly)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ConvertTo-ClaudeHookPayloadResult {
|
|
112
|
+
<#
|
|
113
|
+
.SYNOPSIS
|
|
114
|
+
Build the typed result every parse/extract function returns: IsValid, Value
|
|
115
|
+
(the parsed envelope or extracted tool_input), and Anomaly (null when
|
|
116
|
+
valid). Always an object, so no caller tests for null first.
|
|
117
|
+
.OUTPUTS
|
|
118
|
+
System.Management.Automation.PSCustomObject
|
|
119
|
+
#>
|
|
120
|
+
[CmdletBinding()]
|
|
121
|
+
[OutputType([pscustomobject])]
|
|
122
|
+
param(
|
|
123
|
+
[Parameter(Mandatory)]
|
|
124
|
+
[bool] $IsValid,
|
|
125
|
+
|
|
126
|
+
[AllowNull()]
|
|
127
|
+
[object] $Value,
|
|
128
|
+
|
|
129
|
+
[AllowNull()]
|
|
130
|
+
[AllowEmptyString()]
|
|
131
|
+
[string] $Anomaly
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
$normalizedAnomaly = $null
|
|
135
|
+
if (-not [string]::IsNullOrWhiteSpace($Anomaly)) {
|
|
136
|
+
$normalizedAnomaly = [string]$Anomaly
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [pscustomobject]@{
|
|
140
|
+
IsValid = $IsValid
|
|
141
|
+
Value = $Value
|
|
142
|
+
Anomaly = $normalizedAnomaly
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Read-ClaudeHookRawPayload {
|
|
147
|
+
<#
|
|
148
|
+
.SYNOPSIS
|
|
149
|
+
Acquire the raw hook payload text, stdin first.
|
|
150
|
+
|
|
151
|
+
.DESCRIPTION
|
|
152
|
+
Ordering: stdin, then the CLAUDE_HOOK_INPUT environment variable, then the
|
|
153
|
+
CLAUDE_TOOL_INPUT environment variable. The first non-whitespace source
|
|
154
|
+
wins; when every source is empty or whitespace the function returns an empty
|
|
155
|
+
string, which callers classify as the EmptyPayload anomaly.
|
|
156
|
+
|
|
157
|
+
The stdin read is guarded by a redirect probe. An unguarded
|
|
158
|
+
[Console]::In.ReadToEnd() blocks indefinitely on a non-redirected console,
|
|
159
|
+
which would hang the documented manual/CLI invocation of validate-bash.ps1.
|
|
160
|
+
The guard lives in this function body rather than inside the read seam's
|
|
161
|
+
default so a test can drive both polarities by injection. A stdin read that
|
|
162
|
+
throws falls back rather than propagating (persist-session-id.ps1 precedent).
|
|
163
|
+
|
|
164
|
+
.PARAMETER ReadStandardInput
|
|
165
|
+
Seam for the stdin read. Default is the bare [Console]::In.ReadToEnd().
|
|
166
|
+
.PARAMETER TestStandardInputRedirected
|
|
167
|
+
Seam for the redirect probe. Default is [Console]::IsInputRedirected. When
|
|
168
|
+
it evaluates falsey, stdin is treated as empty and the env fallback runs.
|
|
169
|
+
.PARAMETER HookInputFallback
|
|
170
|
+
Seam for the first environment fallback.
|
|
171
|
+
.PARAMETER ToolInputFallback
|
|
172
|
+
Seam for the second environment fallback.
|
|
173
|
+
.OUTPUTS
|
|
174
|
+
System.String
|
|
175
|
+
#>
|
|
176
|
+
[CmdletBinding()]
|
|
177
|
+
[OutputType([string])]
|
|
178
|
+
param(
|
|
179
|
+
[scriptblock] $ReadStandardInput = { [Console]::In.ReadToEnd() },
|
|
180
|
+
|
|
181
|
+
[scriptblock] $TestStandardInputRedirected = { [Console]::IsInputRedirected },
|
|
182
|
+
|
|
183
|
+
[AllowNull()]
|
|
184
|
+
[AllowEmptyString()]
|
|
185
|
+
[string] $HookInputFallback = $env:CLAUDE_HOOK_INPUT,
|
|
186
|
+
|
|
187
|
+
[AllowNull()]
|
|
188
|
+
[AllowEmptyString()]
|
|
189
|
+
[string] $ToolInputFallback = $env:CLAUDE_TOOL_INPUT
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
$raw = ''
|
|
193
|
+
|
|
194
|
+
$isRedirected = $false
|
|
195
|
+
try {
|
|
196
|
+
$isRedirected = [bool](& $TestStandardInputRedirected)
|
|
197
|
+
} catch {
|
|
198
|
+
$isRedirected = $false
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if ($isRedirected) {
|
|
202
|
+
try {
|
|
203
|
+
$raw = [string](& $ReadStandardInput)
|
|
204
|
+
} catch {
|
|
205
|
+
$raw = ''
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (-not [string]::IsNullOrWhiteSpace($raw)) {
|
|
210
|
+
return $raw
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (-not [string]::IsNullOrWhiteSpace($HookInputFallback)) {
|
|
214
|
+
return [string]$HookInputFallback
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (-not [string]::IsNullOrWhiteSpace($ToolInputFallback)) {
|
|
218
|
+
return [string]$ToolInputFallback
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return ''
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function ConvertFrom-ClaudeHookEnvelope {
|
|
225
|
+
<#
|
|
226
|
+
.SYNOPSIS
|
|
227
|
+
Parse raw hook payload text into the envelope object.
|
|
228
|
+
|
|
229
|
+
.DESCRIPTION
|
|
230
|
+
Strips a UTF-8 byte-order mark and normalizes CRLF before parsing, because
|
|
231
|
+
stdin text arrives with either line ending and a BOM survives a piped read.
|
|
232
|
+
Never returns a silent null for malformed input: an empty or whitespace-only
|
|
233
|
+
payload yields the EmptyPayload anomaly and unparseable text yields the
|
|
234
|
+
UnparseableJson anomaly.
|
|
235
|
+
|
|
236
|
+
.PARAMETER Raw
|
|
237
|
+
The raw payload text from Read-ClaudeHookRawPayload.
|
|
238
|
+
.OUTPUTS
|
|
239
|
+
System.Management.Automation.PSCustomObject (typed result)
|
|
240
|
+
#>
|
|
241
|
+
[CmdletBinding()]
|
|
242
|
+
[OutputType([pscustomobject])]
|
|
243
|
+
param(
|
|
244
|
+
[AllowNull()]
|
|
245
|
+
[AllowEmptyString()]
|
|
246
|
+
[string] $Raw
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if ([string]::IsNullOrWhiteSpace($Raw)) {
|
|
250
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyEmptyPayload
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# A BOM decoded into the string appears as U+FEFF and makes ConvertFrom-Json fail.
|
|
254
|
+
$text = ([string]$Raw).TrimStart([char]0xFEFF)
|
|
255
|
+
$text = $text.Replace("`r`n", "`n")
|
|
256
|
+
|
|
257
|
+
if ([string]::IsNullOrWhiteSpace($text)) {
|
|
258
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyEmptyPayload
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
$envelope = $text | ConvertFrom-Json -ErrorAction Stop
|
|
263
|
+
} catch {
|
|
264
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyUnparseableJson
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if ($null -eq $envelope) {
|
|
268
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyUnparseableJson
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $true -Value $envelope -Anomaly $null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function Test-ClaudeHookEnvelopeHasKey {
|
|
275
|
+
<#
|
|
276
|
+
.SYNOPSIS
|
|
277
|
+
Report whether a parsed JSON object (PSCustomObject or dictionary) carries a
|
|
278
|
+
named key, without throwing under StrictMode.
|
|
279
|
+
.OUTPUTS
|
|
280
|
+
System.Boolean
|
|
281
|
+
#>
|
|
282
|
+
[CmdletBinding()]
|
|
283
|
+
[OutputType([bool])]
|
|
284
|
+
param(
|
|
285
|
+
[AllowNull()]
|
|
286
|
+
[object] $Envelope,
|
|
287
|
+
|
|
288
|
+
[Parameter(Mandatory)]
|
|
289
|
+
[string] $Name
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if ($null -eq $Envelope) {
|
|
293
|
+
return $false
|
|
294
|
+
}
|
|
295
|
+
if ($Envelope -is [System.Collections.IDictionary]) {
|
|
296
|
+
return $Envelope.Contains($Name)
|
|
297
|
+
}
|
|
298
|
+
if ($Envelope -isnot [psobject]) {
|
|
299
|
+
return $false
|
|
300
|
+
}
|
|
301
|
+
return (@($Envelope.PSObject.Properties.Name) -contains $Name)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function Get-ClaudeHookEnvelopeValue {
|
|
305
|
+
<#
|
|
306
|
+
.SYNOPSIS
|
|
307
|
+
Read a named value off a parsed JSON object, returning null when the key is
|
|
308
|
+
absent rather than throwing under StrictMode.
|
|
309
|
+
.OUTPUTS
|
|
310
|
+
System.Object or null
|
|
311
|
+
#>
|
|
312
|
+
[CmdletBinding()]
|
|
313
|
+
param(
|
|
314
|
+
[AllowNull()]
|
|
315
|
+
[object] $Envelope,
|
|
316
|
+
|
|
317
|
+
[Parameter(Mandatory)]
|
|
318
|
+
[string] $Name
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if (-not (Test-ClaudeHookEnvelopeHasKey -Envelope $Envelope -Name $Name)) {
|
|
322
|
+
return $null
|
|
323
|
+
}
|
|
324
|
+
if ($Envelope -is [System.Collections.IDictionary]) {
|
|
325
|
+
return $Envelope[$Name]
|
|
326
|
+
}
|
|
327
|
+
return $Envelope.PSObject.Properties[$Name].Value
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function Test-ClaudeHookObjectValue {
|
|
331
|
+
<#
|
|
332
|
+
.SYNOPSIS
|
|
333
|
+
Report whether a parsed JSON value is an object rather than a scalar or an
|
|
334
|
+
array. ConvertFrom-Json produces a PSCustomObject for a JSON object, a
|
|
335
|
+
native scalar for a scalar, and an array for a JSON array; only the first is
|
|
336
|
+
a usable tool_input.
|
|
337
|
+
.OUTPUTS
|
|
338
|
+
System.Boolean
|
|
339
|
+
#>
|
|
340
|
+
[CmdletBinding()]
|
|
341
|
+
[OutputType([bool])]
|
|
342
|
+
param(
|
|
343
|
+
[AllowNull()]
|
|
344
|
+
[object] $Value
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if ($null -eq $Value) {
|
|
348
|
+
return $false
|
|
349
|
+
}
|
|
350
|
+
if ($Value -is [System.Collections.IDictionary]) {
|
|
351
|
+
return $true
|
|
352
|
+
}
|
|
353
|
+
if ($Value -is [string] -or $Value -is [bool] -or $Value -is [ValueType]) {
|
|
354
|
+
return $false
|
|
355
|
+
}
|
|
356
|
+
if ($Value -is [System.Collections.IEnumerable]) {
|
|
357
|
+
return $false
|
|
358
|
+
}
|
|
359
|
+
return ($Value -is [psobject])
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function Get-ClaudeHookToolInput {
|
|
363
|
+
<#
|
|
364
|
+
.SYNOPSIS
|
|
365
|
+
Extract the envelope's nested tool_input object. Strict: no flat-root
|
|
366
|
+
fallback.
|
|
367
|
+
|
|
368
|
+
.DESCRIPTION
|
|
369
|
+
A parsed payload with no tool_input key is the MissingToolInput anomaly,
|
|
370
|
+
which is exactly what the legacy flat root shape produces. A tool_input
|
|
371
|
+
present but null is NullToolInput; present but not an object is
|
|
372
|
+
NonObjectToolInput. Absence of a property inside a well-formed tool_input is
|
|
373
|
+
not handled here at all: that stays each hook's own scope filter.
|
|
374
|
+
|
|
375
|
+
.PARAMETER Envelope
|
|
376
|
+
The parsed envelope object from ConvertFrom-ClaudeHookEnvelope.
|
|
377
|
+
.OUTPUTS
|
|
378
|
+
System.Management.Automation.PSCustomObject (typed result)
|
|
379
|
+
#>
|
|
380
|
+
[CmdletBinding()]
|
|
381
|
+
[OutputType([pscustomobject])]
|
|
382
|
+
param(
|
|
383
|
+
[AllowNull()]
|
|
384
|
+
[object] $Envelope
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if ($null -eq $Envelope) {
|
|
388
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyMissingToolInput
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (-not (Test-ClaudeHookEnvelopeHasKey -Envelope $Envelope -Name $script:ToolInputKey)) {
|
|
392
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyMissingToolInput
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
$toolInput = Get-ClaudeHookEnvelopeValue -Envelope $Envelope -Name $script:ToolInputKey
|
|
396
|
+
|
|
397
|
+
if ($null -eq $toolInput) {
|
|
398
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyNullToolInput
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (-not (Test-ClaudeHookObjectValue -Value $toolInput)) {
|
|
402
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyNonObjectToolInput
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return ConvertTo-ClaudeHookPayloadResult -IsValid $true -Value $toolInput -Anomaly $null
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function Get-ClaudeHookToolInputString {
|
|
409
|
+
<#
|
|
410
|
+
.SYNOPSIS
|
|
411
|
+
Read a named string property off an already-extracted tool_input object.
|
|
412
|
+
|
|
413
|
+
.DESCRIPTION
|
|
414
|
+
Property-level tolerance lives here: an absent property returns an empty
|
|
415
|
+
string, which every hook already treats as "out of my scope, allow". This
|
|
416
|
+
helper exists only so the hooks do not each re-implement the StrictMode-safe
|
|
417
|
+
property probe.
|
|
418
|
+
|
|
419
|
+
.OUTPUTS
|
|
420
|
+
System.String
|
|
421
|
+
#>
|
|
422
|
+
[CmdletBinding()]
|
|
423
|
+
[OutputType([string])]
|
|
424
|
+
param(
|
|
425
|
+
[AllowNull()]
|
|
426
|
+
[object] $ToolInput,
|
|
427
|
+
|
|
428
|
+
[Parameter(Mandatory)]
|
|
429
|
+
[string] $Name
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
$value = Get-ClaudeHookEnvelopeValue -Envelope $ToolInput -Name $Name
|
|
433
|
+
if ($null -eq $value) {
|
|
434
|
+
return ''
|
|
435
|
+
}
|
|
436
|
+
return [string]$value
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function Resolve-ClaudeHookToolInput {
|
|
440
|
+
<#
|
|
441
|
+
.SYNOPSIS
|
|
442
|
+
One-call convenience: raw text in, typed tool_input result out.
|
|
443
|
+
|
|
444
|
+
.DESCRIPTION
|
|
445
|
+
Composes ConvertFrom-ClaudeHookEnvelope and Get-ClaudeHookToolInput and
|
|
446
|
+
additionally surfaces the parsed envelope on the Envelope member, because
|
|
447
|
+
enforce-epic-invocation-origin.ps1 needs the envelope root (agent_type)
|
|
448
|
+
alongside the nested tool_input. Returns the envelope-parse anomaly unchanged
|
|
449
|
+
when the parse fails, so a hook maps a single Anomaly value to a single deny
|
|
450
|
+
reason.
|
|
451
|
+
|
|
452
|
+
.PARAMETER Raw
|
|
453
|
+
The raw payload text from Read-ClaudeHookRawPayload.
|
|
454
|
+
.OUTPUTS
|
|
455
|
+
System.Management.Automation.PSCustomObject
|
|
456
|
+
#>
|
|
457
|
+
[CmdletBinding()]
|
|
458
|
+
[OutputType([pscustomobject])]
|
|
459
|
+
param(
|
|
460
|
+
[AllowNull()]
|
|
461
|
+
[AllowEmptyString()]
|
|
462
|
+
[string] $Raw
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
$parsed = ConvertFrom-ClaudeHookEnvelope -Raw $Raw
|
|
466
|
+
if (-not $parsed.IsValid) {
|
|
467
|
+
return [pscustomobject]@{
|
|
468
|
+
IsValid = $false
|
|
469
|
+
Value = $null
|
|
470
|
+
Envelope = $null
|
|
471
|
+
Anomaly = $parsed.Anomaly
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
$extracted = Get-ClaudeHookToolInput -Envelope $parsed.Value
|
|
476
|
+
return [pscustomobject]@{
|
|
477
|
+
IsValid = $extracted.IsValid
|
|
478
|
+
Value = $extracted.Value
|
|
479
|
+
Envelope = $parsed.Value
|
|
480
|
+
Anomaly = $extracted.Anomaly
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
Export-ModuleMember -Function `
|
|
485
|
+
Read-ClaudeHookRawPayload, `
|
|
486
|
+
ConvertFrom-ClaudeHookEnvelope, `
|
|
487
|
+
Get-ClaudeHookToolInput, `
|
|
488
|
+
Resolve-ClaudeHookToolInput, `
|
|
489
|
+
Get-ClaudeHookEnvelopeValue, `
|
|
490
|
+
Test-ClaudeHookEnvelopeHasKey, `
|
|
491
|
+
Test-ClaudeHookObjectValue, `
|
|
492
|
+
Get-ClaudeHookToolInputString, `
|
|
493
|
+
Get-ClaudeHookPayloadAnomalyCode, `
|
|
494
|
+
Get-ClaudeHookPayloadAnomalyReason
|