@windyroad/risk-scorer 0.9.0-preview.313 → 0.9.0-preview.315

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.
@@ -10,14 +10,14 @@ model: inherit
10
10
 
11
11
  You are the External-Comms Risk Reviewer. Your single job: read the draft of an outbound prose tool call (a `gh issue create --body ...`, a PR description, a security-advisory body, a `.changeset/*.md` file, or the README diff that `npm publish` will publish) and return a structured PASS/FAIL verdict against RISK-POLICY.md's Confidential Information classes.
12
12
 
13
- You are read-only. You do NOT write files, do NOT commit, do NOT modify the draft. Your verdict is consumed by the `risk-score-mark.sh` PostToolUse hook (P064 / ADR-028 amended), which writes the marker that allows the gated tool call to proceed.
13
+ You are read-only. You do NOT write files, do NOT commit, do NOT modify the draft. Your verdict is consumed by the `risk-score-mark.sh` PostToolUse hook (P064 / ADR-028 amended 2026-05-14 + 2026-05-16), which derives the marker key from the prompt structure you receive and writes the marker that allows the gated tool call to proceed.
14
14
 
15
15
  ## What you receive
16
16
 
17
- The invoking skill (`/wr-risk-scorer:assess-external-comms`) or the agent that hit the gate provides:
17
+ The invoking skill (`/wr-risk-scorer:assess-external-comms`) or the agent that hit the gate provides a structured prompt (P166 / ADR-028 amended 2026-05-16):
18
18
 
19
- - The **draft body** verbatimthe exact prose that would land on the external surface.
20
- - The **target surface** one of: `gh-issue-create`, `gh-issue-comment`, `gh-issue-edit`, `gh-pr-create`, `gh-pr-comment`, `gh-pr-edit`, `gh-api-security-advisories`, `gh-api-comments`, `npm-publish`, `changeset-author`.
19
+ - A leading `SURFACE: <name>` line one of: `gh-issue-create`, `gh-issue-comment`, `gh-issue-edit`, `gh-pr-create`, `gh-pr-comment`, `gh-pr-edit`, `gh-api-security-advisories`, `gh-api-comments`, `npm-publish`, `changeset-author`.
20
+ - The **draft body** verbatim, wrapped in `<draft>...</draft>` markers so the PostToolUse hook can extract it for marker-key derivation.
21
21
  - The **destination** when known (e.g. `anthropics/claude-code#52831`).
22
22
 
23
23
  Read `RISK-POLICY.md` (project root) to get the authoritative Confidential Information class list. As of P064 it covers:
@@ -42,28 +42,20 @@ The hybrid pre-filter (`packages/*/hooks/lib/leak-detect.sh`) has already caught
42
42
 
43
43
  ## Verdict format (MANDATORY)
44
44
 
45
- End your report with a structured block consumed by `risk-score-mark.sh`. Every field is required.
45
+ End your report with a structured block consumed by `risk-score-mark.sh`:
46
46
 
47
47
  ```
48
48
  EXTERNAL_COMMS_RISK_VERDICT: PASS
49
- EXTERNAL_COMMS_RISK_KEY: <sha256 hex string>
50
49
  ```
51
50
 
52
51
  OR for a failed review:
53
52
 
54
53
  ```
55
54
  EXTERNAL_COMMS_RISK_VERDICT: FAIL
56
- EXTERNAL_COMMS_RISK_KEY: <sha256 hex string>
57
55
  EXTERNAL_COMMS_RISK_REASON: <one-line description of the leak class + matched fragment>
58
56
  ```
59
57
 
60
- Compute the key as:
61
-
62
- ```
63
- printf '%s\n%s' "<draft body verbatim>" "<surface name>" | shasum -a 256 | cut -d' ' -f1
64
- ```
65
-
66
- The key MUST match the gate's computation exactly — a key mismatch means the marker is written for a different draft and the original gated call will continue to deny.
58
+ You do NOT need to emit `EXTERNAL_COMMS_RISK_KEY`. The PostToolUse hook derives the marker key directly from the `SURFACE:` line and `<draft>...</draft>` block in the prompt you received (P166 / ADR-028 amended 2026-05-16). Single fire per gate cycle.
67
59
 
68
60
  ## Grounding (ADR-026)
69
61
 
@@ -82,7 +74,7 @@ Example:
82
74
  - You are a reviewer, not an editor — do NOT propose rewrites in the verdict block. (Free prose suggestions outside the verdict block are fine and helpful.)
83
75
  - Do NOT score by analogy when the policy names the class.
84
76
  - Do NOT write to `/tmp/` or any marker location yourself — the PostToolUse hook owns that.
85
- - Do NOT skip the `EXTERNAL_COMMS_RISK_KEY` line; without it, the marker hook has no key to write the marker against and the gate will deny again on retry.
77
+ - You do NOT need to emit `EXTERNAL_COMMS_RISK_KEY` the hook derives the key from the prompt's `SURFACE:` + `<draft>` structure (P166 / ADR-028 amended 2026-05-16). If your prompt lacks that structure (legacy caller), the hook falls back to an emitted KEY line for backward compatibility, but the canonical path is hook-side derivation.
86
78
  - When the draft is empty (e.g. `npm publish` with no extractable body fragment), review the staged content the publish would push (README diff, package.json description) instead. If neither is available, FAIL with reason "draft body unresolvable; cannot risk-review without text" so the user can pre-review manually.
87
79
 
88
80
  ## Below-Appetite Output Rule (ADR-013 Rule 5)
@@ -31,7 +31,12 @@
31
31
  # Marker location: ${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}/external-comms-<EVALUATOR_ID>-reviewed-<sha256>
32
32
  # Marker writer: PostToolUse:Agent hook in each consumer plugin
33
33
  # (risk-score-mark.sh or external-comms-mark-reviewed.sh) on
34
- # subagent type wr-<plugin>:external-comms.
34
+ # subagent type wr-<plugin>:external-comms. The mark hook
35
+ # derives the marker key from the agent's tool_input.prompt
36
+ # by parsing the same `SURFACE:` + `<draft>` structure the
37
+ # orchestrator was instructed to include (P166 / ADR-028
38
+ # amended 2026-05-16). Single fire per gate cycle suffices;
39
+ # the agent no longer needs to compute the key itself.
35
40
  #
36
41
  # Per-evaluator marker scheme (ADR-028 amended 2026-05-14): when both
37
42
  # voice-tone and risk-scorer are installed, both gates fire on the same
@@ -234,8 +239,12 @@ if [ -f "$MARKER" ]; then
234
239
  fi
235
240
 
236
241
  # Marker absent — deny + delegate.
242
+ # P166: instruct the orchestrator to structure the agent prompt with a
243
+ # leading `SURFACE: <name>` line and a `<draft>...</draft>` block so the
244
+ # PostToolUse mark hook can derive the canonical marker key locally
245
+ # (sha256(DRAFT + '\n' + SURFACE)). Single fire per gate cycle.
237
246
  VERDICT_PREFIX="${EXTERNAL_COMMS_VERDICT_PREFIX:-EXTERNAL_COMMS_${EXTERNAL_COMMS_EVALUATOR_ID^^}}"
238
- REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s draft has not been reviewed by %s. Delegate to %s (subagent_type: '"'"'%s'"'"') with the draft body for review. The PostToolUse hook will mark this draft reviewed when the subagent emits %s_VERDICT: PASS. Use %s for an interactive walkthrough. Override only when intentional: BYPASS_RISK_GATE=1.' \
239
- "$EXTERNAL_COMMS_EVALUATOR_ID" "$SURFACE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$VERDICT_PREFIX" "$EXTERNAL_COMMS_ASSESS_SKILL")
247
+ REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s draft has not been reviewed by %s. Delegate to %s (subagent_type: '"'"'%s'"'"') with a prompt that starts with the line `SURFACE: %s` and wraps the draft body verbatim inside `<draft>...</draft>` markers. The PostToolUse hook derives the marker key from that structure and marks the draft reviewed when the subagent emits %s_VERDICT: PASS — single fire suffices. Use %s for an interactive walkthrough. Override only when intentional: BYPASS_RISK_GATE=1.' \
248
+ "$EXTERNAL_COMMS_EVALUATOR_ID" "$SURFACE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$SURFACE" "$VERDICT_PREFIX" "$EXTERNAL_COMMS_ASSESS_SKILL")
240
249
  deny_with_reason "$REASON"
241
250
  exit 0
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # Shared helper: derive the external-comms marker key from an agent's
3
+ # tool_input.prompt by extracting the structured `SURFACE: <name>` line
4
+ # and `<draft>...</draft>` block, then computing
5
+ # sha256(DRAFT + '\n' + SURFACE) — the same key shape the gate computes
6
+ # at PreToolUse time (external-comms-gate.sh line 229).
7
+ #
8
+ # P166 + ADR-028 amended 2026-05-16: the PostToolUse:Agent mark hook
9
+ # derives the marker key from observed runtime state instead of trusting
10
+ # an agent-emitted EXTERNAL_COMMS_<EVAL>_KEY line. Removes the
11
+ # double-invocation cost class — single fire per gate cycle suffices.
12
+ #
13
+ # Canonical source: packages/shared/hooks/lib/external-comms-key.sh
14
+ # Synced byte-identically into each consumer plugin's hooks/lib/ via
15
+ # scripts/sync-external-comms-gate.sh (ADR-017 duplicate-script pattern).
16
+ #
17
+ # Returns the 64-char hex sha256 on stdout when both markers are present
18
+ # in the prompt. Returns empty string when either marker is absent — the
19
+ # caller falls back to the agent-emitted KEY for backward compatibility
20
+ # with cached old SKILL.md / agent prompts.
21
+
22
+ derive_external_comms_key_from_prompt() {
23
+ local prompt="$1"
24
+ [ -n "$prompt" ] || { echo ""; return 0; }
25
+ printf '%s' "$prompt" | python3 -c "
26
+ import sys, re, hashlib
27
+ text = sys.stdin.read()
28
+ # DRAFT extraction: non-greedy match between <draft>...</draft>.
29
+ # Tolerates an optional newline immediately after <draft> and before </draft>
30
+ # so the body content does not capture wrapping newlines.
31
+ draft_match = re.search(r'<draft>\n?(.*?)\n?</draft>', text, re.DOTALL)
32
+ # SURFACE extraction: must be anchored to line start (MULTILINE) to avoid
33
+ # matching prose like 'context says SURFACE: x'. Surface name is a single
34
+ # token: letter + word/hyphen chars.
35
+ surface_match = re.search(r'^SURFACE:\s*([A-Za-z][\w-]*)', text, re.MULTILINE)
36
+ if not draft_match or not surface_match:
37
+ print('')
38
+ sys.exit(0)
39
+ draft = draft_match.group(1)
40
+ surface = surface_match.group(1)
41
+ payload = (draft + '\n' + surface).encode('utf-8')
42
+ print(hashlib.sha256(payload).hexdigest())
43
+ " 2>/dev/null
44
+ }
@@ -11,6 +11,8 @@ set -euo pipefail
11
11
 
12
12
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
13
  source "$SCRIPT_DIR/lib/gate-helpers.sh"
14
+ # shellcheck source=lib/external-comms-key.sh
15
+ source "$SCRIPT_DIR/lib/external-comms-key.sh"
14
16
  _enable_err_trap
15
17
 
16
18
  _parse_input
@@ -204,18 +206,43 @@ if echo "$SUBAGENT" | grep -qE 'risk-scorer.policy'; then
204
206
  fi
205
207
 
206
208
  # ---------------------------------------------------------------------------
207
- # External-comms reviewer (P064 / ADR-028 amended 2026-05-14): write
208
- # per-evaluator marker keyed on sha256(draft + '\n' + surface). Subagent
209
- # emits the key; this hook trusts and uses it. Marker file:
210
- # external-comms-risk-reviewed-<key>. The voice-tone evaluator (P038)
211
- # writes its own peer marker external-comms-voice-tone-reviewed-<key>
212
- # from packages/voice-tone/hooks/external-comms-mark-reviewed.sh.
209
+ # External-comms reviewer (P064 / ADR-028 amended 2026-05-14, further
210
+ # amended 2026-05-16 P166): write per-evaluator marker keyed on
211
+ # sha256(draft + '\n' + surface). The hook derives the key from the
212
+ # agent's tool_input.prompt (structured `SURFACE:` line + `<draft>`
213
+ # block) instead of trusting an agent-emitted KEY — single fire per
214
+ # gate cycle suffices. Backward-compat fallback to the agent's
215
+ # EXTERNAL_COMMS_RISK_KEY line preserved during the deprecation window
216
+ # (one release cycle).
217
+ # Marker file: external-comms-risk-reviewed-<key>. The voice-tone
218
+ # evaluator (P038) writes its own peer marker
219
+ # external-comms-voice-tone-reviewed-<key> from
220
+ # packages/voice-tone/hooks/external-comms-mark-reviewed.sh.
213
221
  # ---------------------------------------------------------------------------
214
222
  if echo "$SUBAGENT" | grep -qE 'risk-scorer.external-comms'; then
215
223
  VERDICT_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^EXTERNAL_COMMS_RISK_VERDICT:' | tail -1) || true
216
- KEY_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^EXTERNAL_COMMS_RISK_KEY:' | tail -1) || true
217
224
  VERDICT=$(echo "$VERDICT_LINE" | sed 's/^EXTERNAL_COMMS_RISK_VERDICT:[[:space:]]*//' | tr -d '[:space:]')
218
- KEY=$(echo "$KEY_LINE" | sed 's/^EXTERNAL_COMMS_RISK_KEY:[[:space:]]*//' | tr -d '[:space:]')
225
+
226
+ # Read the prompt the orchestrator sent to the agent so we can derive
227
+ # the canonical key locally. _HOOK_INPUT is set by gate-helpers.sh's
228
+ # _parse_input upstream of this branch.
229
+ PROMPT=$(echo "$_HOOK_INPUT" | python3 -c "
230
+ import sys, json
231
+ try:
232
+ print(json.load(sys.stdin).get('tool_input', {}).get('prompt', ''))
233
+ except Exception:
234
+ print('')
235
+ " 2>/dev/null || echo "")
236
+
237
+ # Primary: derive from the prompt (P166 single-fire path).
238
+ KEY=$(derive_external_comms_key_from_prompt "$PROMPT")
239
+ if [ -z "$KEY" ]; then
240
+ # Fallback: cached old SKILL.md still instructs the agent to emit
241
+ # EXTERNAL_COMMS_RISK_KEY. Honour it during the deprecation window.
242
+ KEY_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^EXTERNAL_COMMS_RISK_KEY:' | tail -1) || true
243
+ KEY=$(echo "$KEY_LINE" | sed 's/^EXTERNAL_COMMS_RISK_KEY:[[:space:]]*//' | tr -d '[:space:]')
244
+ fi
245
+
219
246
  # Validate key: 64 hex chars (sha256 output). Reject anything else.
220
247
  if echo "$KEY" | grep -qE '^[0-9a-f]{64}$'; then
221
248
  case "$VERDICT" in
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural tests for risk-score-mark.sh external-comms branch under
3
+ # P166 hook-side key derivation (ADR-028 amended 2026-05-16).
4
+ #
5
+ # Contract: the PostToolUse:Agent hook derives the marker key from
6
+ # tool_input.prompt's `SURFACE: <name>` + `<draft>...</draft>` structure
7
+ # instead of trusting an agent-emitted EXTERNAL_COMMS_RISK_KEY line.
8
+ # On PASS, writes external-comms-risk-reviewed-<KEY> at the derived key.
9
+ # Backward-compat: falls back to agent-emitted KEY when prompt has no
10
+ # structure (one release-cycle window).
11
+
12
+ setup() {
13
+ SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
14
+ HOOK="$SCRIPT_DIR/risk-score-mark.sh"
15
+ ORIG_DIR="$PWD"
16
+ TEST_DIR=$(mktemp -d)
17
+ cd "$TEST_DIR"
18
+ TMPDIR="$TEST_DIR/tmp"
19
+ export TMPDIR
20
+ mkdir -p "$TMPDIR"
21
+ SESSION_ID="test-rs-mark-extcomms-prompt-$$-${BATS_TEST_NUMBER}"
22
+ RDIR="$TMPDIR/claude-risk-${SESSION_ID}"
23
+ }
24
+
25
+ teardown() {
26
+ cd "$ORIG_DIR"
27
+ rm -rf "$TEST_DIR"
28
+ }
29
+
30
+ gate_key() {
31
+ local draft="$1" surface="$2"
32
+ printf '%s\n%s' "$draft" "$surface" | shasum -a 256 | cut -d' ' -f1
33
+ }
34
+
35
+ run_hook() {
36
+ local prompt="$1"
37
+ local agent_output="$2"
38
+ python3 -c "
39
+ import json, sys
40
+ print(json.dumps({
41
+ 'tool_name': 'Agent',
42
+ 'session_id': '${SESSION_ID}',
43
+ 'tool_input': {'subagent_type': 'wr-risk-scorer:external-comms', 'prompt': sys.argv[1]},
44
+ 'tool_response': {'content': [{'type': 'text', 'text': sys.argv[2]}]}
45
+ }))" "$prompt" "$agent_output" | bash "$HOOK"
46
+ }
47
+
48
+ @test "external-comms PASS with structured prompt: marker lands at hook-derived key" {
49
+ DRAFT="we observed a leaked secret pattern in the changeset"
50
+ SURFACE="changeset-author"
51
+ PROMPT=$'SURFACE: '"$SURFACE"$'\n<draft>\n'"$DRAFT"$'\n</draft>\nReview against RISK-POLICY.md.'
52
+ AGENT_OUTPUT=$'no Confidential Information class matched\nEXTERNAL_COMMS_RISK_VERDICT: PASS'
53
+ run_hook "$PROMPT" "$AGENT_OUTPUT"
54
+ KEY=$(gate_key "$DRAFT" "$SURFACE")
55
+ [ -f "$RDIR/external-comms-risk-reviewed-${KEY}" ]
56
+ }
57
+
58
+ @test "external-comms FAIL with structured prompt: no marker" {
59
+ DRAFT="client Acme Corp is hitting this"
60
+ SURFACE="gh-issue-create"
61
+ PROMPT=$'SURFACE: '"$SURFACE"$'\n<draft>\n'"$DRAFT"$'\n</draft>'
62
+ AGENT_OUTPUT=$'EXTERNAL_COMMS_RISK_VERDICT: FAIL\nEXTERNAL_COMMS_RISK_REASON: Client names class — "Acme Corp"'
63
+ run_hook "$PROMPT" "$AGENT_OUTPUT"
64
+ KEY=$(gate_key "$DRAFT" "$SURFACE")
65
+ [ ! -f "$RDIR/external-comms-risk-reviewed-${KEY}" ]
66
+ }
67
+
68
+ @test "external-comms PASS with structured prompt AND agent-emitted KEY: hook-derived key wins" {
69
+ DRAFT="hook-derived wins"
70
+ SURFACE="gh-pr-comment"
71
+ PROMPT=$'SURFACE: '"$SURFACE"$'\n<draft>\n'"$DRAFT"$'\n</draft>'
72
+ BOGUS_KEY="0000000000000000000000000000000000000000000000000000000000000000"
73
+ AGENT_OUTPUT=$'EXTERNAL_COMMS_RISK_VERDICT: PASS\nEXTERNAL_COMMS_RISK_KEY: '"$BOGUS_KEY"
74
+ run_hook "$PROMPT" "$AGENT_OUTPUT"
75
+ DERIVED_KEY=$(gate_key "$DRAFT" "$SURFACE")
76
+ [ -f "$RDIR/external-comms-risk-reviewed-${DERIVED_KEY}" ]
77
+ [ ! -f "$RDIR/external-comms-risk-reviewed-${BOGUS_KEY}" ]
78
+ }
79
+
80
+ @test "external-comms backward-compat: PASS with no structured prompt but agent KEY" {
81
+ LEGACY_KEY="fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
82
+ PROMPT="legacy unstructured prompt"
83
+ AGENT_OUTPUT=$'EXTERNAL_COMMS_RISK_VERDICT: PASS\nEXTERNAL_COMMS_RISK_KEY: '"$LEGACY_KEY"
84
+ run_hook "$PROMPT" "$AGENT_OUTPUT"
85
+ [ -f "$RDIR/external-comms-risk-reviewed-${LEGACY_KEY}" ]
86
+ }
87
+
88
+ @test "external-comms no structured prompt and no agent KEY: no marker" {
89
+ PROMPT="legacy"
90
+ AGENT_OUTPUT=$'EXTERNAL_COMMS_RISK_VERDICT: PASS'
91
+ run_hook "$PROMPT" "$AGENT_OUTPUT"
92
+ ext_markers=$(find "$RDIR" -maxdepth 1 -name 'external-comms-risk-reviewed-*' 2>/dev/null | wc -l | tr -d ' ')
93
+ [ "$ext_markers" -eq 0 ]
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.9.0-preview.313",
3
+ "version": "0.9.0-preview.315",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"
@@ -51,12 +51,24 @@ Do not ask if the surface is obvious from the conversation context.
51
51
 
52
52
  ### 3. Construct the review prompt
53
53
 
54
- Build a self-contained prompt for the `wr-risk-scorer:external-comms` subagent that includes:
54
+ Build a self-contained prompt for the `wr-risk-scorer:external-comms` subagent. The prompt MUST be structured so the PostToolUse hook can derive the marker key locally (P166 / ADR-028 amended 2026-05-16) — single fire per gate cycle suffices:
55
55
 
56
- - The **draft body** verbatim (between explicit `<draft>...</draft>` markers so the agent's substring extraction is unambiguous).
57
- - The **target surface** (one of the canonical strings above).
58
- - The **destination** when known.
59
- - A reminder to compute `EXTERNAL_COMMS_RISK_KEY = sha256(draft + '\n' + surface)`.
56
+ ```
57
+ SURFACE: <surface-name>
58
+ <draft>
59
+ <draft body verbatim>
60
+ </draft>
61
+
62
+ Destination: <destination if known>
63
+ Review against RISK-POLICY.md Confidential Information classes.
64
+ ```
65
+
66
+ Two requirements:
67
+
68
+ - A leading line `SURFACE: <surface-name>` where `<surface-name>` is one of the canonical strings (`gh-issue-create`, `gh-pr-comment`, etc.) — anchored to line start, single token.
69
+ - The **draft body** wrapped verbatim inside `<draft>...</draft>` markers — the hook extracts everything between these markers and uses it for `sha256(DRAFT + '\n' + SURFACE)`.
70
+
71
+ The orchestrator does NOT pre-compute the key — the hook derives it from the prompt structure. Skip the agent-emitted key entirely.
60
72
 
61
73
  ### 4. Delegate to wr-risk-scorer:external-comms
62
74
 
@@ -67,7 +79,7 @@ subagent_type: wr-risk-scorer:external-comms
67
79
  prompt: <constructed review prompt from step 3>
68
80
  ```
69
81
 
70
- Wait for the subagent to complete. The subagent will output a structured verdict block (`EXTERNAL_COMMS_RISK_VERDICT: PASS|FAIL` + `EXTERNAL_COMMS_RISK_KEY: <sha>` + optional `EXTERNAL_COMMS_RISK_REASON: ...`). The `PostToolUse:Agent` hook (`risk-score-mark.sh`) reads that output and writes the marker automatically.
82
+ Wait for the subagent to complete. The subagent outputs a structured verdict block (`EXTERNAL_COMMS_RISK_VERDICT: PASS|FAIL` + optional `EXTERNAL_COMMS_RISK_REASON: ...` on FAIL). The `PostToolUse:Agent` hook (`risk-score-mark.sh`) parses the verdict, derives the marker key from the prompt's `SURFACE:` + `<draft>` structure, and writes the marker automatically on PASS.
71
83
 
72
84
  **Do not write to `${TMPDIR:-/tmp}/claude-risk-*` yourself.** The hook is the only correct mechanism.
73
85