@windyroad/risk-scorer 0.2.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.
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: Denies Edit/Write to RISK-POLICY.md unless the
3
+ # /risk-policy skill has been engaged (marker file exists).
4
+ # Mirrors: architect-enforce-edit.sh
5
+
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
10
+ _enable_err_trap
11
+
12
+ _parse_input
13
+
14
+ FILE_PATH=$(_get_file_path)
15
+ SESSION_ID=$(_get_session_id)
16
+
17
+ if [ -z "$SESSION_ID" ] || [ -z "$FILE_PATH" ]; then
18
+ exit 0
19
+ fi
20
+
21
+ # Only gate RISK-POLICY.md
22
+ BASENAME=$(basename "$FILE_PATH")
23
+ if [ "$BASENAME" != "RISK-POLICY.md" ]; then
24
+ exit 0
25
+ fi
26
+
27
+ # Check for marker
28
+ MARKER="$(_risk_dir "$SESSION_ID")/policy-reviewed"
29
+ if [ -f "$MARKER" ]; then
30
+ exit 0
31
+ fi
32
+
33
+ cat <<'EOF'
34
+ {
35
+ "hookSpecificOutput": {
36
+ "hookEventName": "PreToolUse",
37
+ "permissionDecision": "deny",
38
+ "permissionDecisionReason": "BLOCKED: Cannot edit RISK-POLICY.md directly. Run the /risk-policy skill first -- it enforces ISO 31000 compliance (reads the risk-scorer contract, discovers project context, checks for incidents, validates with you, and smoke-tests the result). Use the Skill tool with skill: \"risk-policy\"."
39
+ }
40
+ }
41
+ EOF
42
+ exit 0
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Stop hook: Clears risk-policy session marker.
3
+ # Mirrors: architect-reset-marker.sh
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
7
+
8
+ _parse_input
9
+
10
+ SESSION_ID=$(_get_session_id)
11
+
12
+ if [ -n "$SESSION_ID" ]; then
13
+ RDIR=$(_risk_dir "$SESSION_ID")
14
+ rm -f "${RDIR}/policy-reviewed" "${RDIR}/plan-reviewed"
15
+ fi
16
+
17
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: Denies git commit when risk policy is stale,
3
+ # commit risk score is missing/expired/drifted/above threshold.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ source "$SCRIPT_DIR/lib/risk-gate.sh"
9
+ _enable_err_trap
10
+
11
+ _parse_input
12
+
13
+ TOOL_NAME=$(_get_tool_name)
14
+ [ "$TOOL_NAME" = "Bash" ] || exit 0
15
+
16
+ COMMAND=$(_get_command)
17
+ echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*git commit' || exit 0
18
+
19
+ SESSION_ID=$(_get_session_id)
20
+ [ -n "$SESSION_ID" ] || exit 0
21
+
22
+ # RISK-POLICY.md must exist and not be stale (>14 days)
23
+ if [ ! -f "RISK-POLICY.md" ] || [ ! -s "RISK-POLICY.md" ]; then
24
+ risk_gate_deny "Commit blocked: RISK-POLICY.md is missing. Run /risk-policy to create it before committing."
25
+ exit 0
26
+ fi
27
+ POLICY_STALE=$(python3 -c "
28
+ from datetime import date
29
+ import re
30
+ try:
31
+ text = open('RISK-POLICY.md').read()
32
+ m = re.search(r'Last reviewed:\*{0,2}\s*(\d{4}-\d{2}-\d{2})', text)
33
+ if m:
34
+ reviewed = date.fromisoformat(m.group(1))
35
+ print('yes' if (date.today() - reviewed).days > 14 else 'no')
36
+ else:
37
+ print('no')
38
+ except:
39
+ print('no')
40
+ " 2>/dev/null || echo "no")
41
+ if [ "$POLICY_STALE" = "yes" ]; then
42
+ risk_gate_deny "Commit blocked: RISK-POLICY.md is stale (last reviewed over 2 weeks ago). Run /risk-policy to update it before committing."
43
+ exit 0
44
+ fi
45
+
46
+ # Clean tree bypass
47
+ RDIR=$(_risk_dir "$SESSION_ID")
48
+ if [ -f "${RDIR}/clean" ]; then
49
+ exit 0
50
+ fi
51
+
52
+ # Risk-reducing/neutral bypass
53
+ if [ -f "${RDIR}/reducing-commit" ]; then
54
+ rm -f "${RDIR}/reducing-commit"
55
+ exit 0
56
+ fi
57
+
58
+ # Gate check: existence, TTL, drift, threshold
59
+ if ! check_risk_gate "$SESSION_ID" "commit"; then
60
+ risk_gate_deny "Commit blocked: ${RISK_GATE_REASON} To proceed: (1) stage files with git add, (2) delegate to risk-scorer-pipeline (subagent_type: 'risk-scorer-pipeline') to assess cumulative pipeline risk. If the commit is risk-neutral or risk-reducing, the scorer will create a bypass marker."
61
+ exit 0
62
+ fi
63
+
64
+ exit 0
@@ -0,0 +1,120 @@
1
+ #!/bin/bash
2
+ # PostToolUse:Agent hook: Deterministically writes all risk score files,
3
+ # verdict markers, and bypass markers by parsing structured output from
4
+ # risk-scorer agents. This is the ONLY place score files are written —
5
+ # agents output structured markers, this hook writes the files.
6
+ #
7
+ # Handles: risk-scorer-pipeline, risk-scorer-plan, risk-scorer-wip, risk-scorer-policy
8
+ # Replaces: risk-policy-mark-reviewed.sh (which had fragile P001 backup parsing)
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
14
+ _enable_err_trap
15
+
16
+ _parse_input
17
+
18
+ TOOL_NAME=$(_get_tool_name)
19
+ [ "$TOOL_NAME" = "Agent" ] || exit 0
20
+
21
+ SUBAGENT=$(_get_subagent_type)
22
+ SESSION_ID=$(_get_session_id)
23
+ [ -n "$SESSION_ID" ] || exit 0
24
+
25
+ # Only handle risk-scorer agents
26
+ case "$SUBAGENT" in
27
+ *risk-scorer*) ;;
28
+ *) exit 0 ;;
29
+ esac
30
+
31
+ AGENT_OUTPUT=$(_get_tool_output)
32
+ RDIR=$(_risk_dir "$SESSION_ID")
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Pipeline scorer: write commit/push/release scores + bypass markers
36
+ # ---------------------------------------------------------------------------
37
+ if echo "$SUBAGENT" | grep -qE 'risk-scorer-pipeline'; then
38
+ # Parse RISK_SCORES: commit=N push=N release=N
39
+ SCORES_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_SCORES:' | tail -1) || true
40
+ if [ -n "$SCORES_LINE" ]; then
41
+ COMMIT=$(echo "$SCORES_LINE" | grep -oE 'commit=[0-9]+' | cut -d= -f2) || true
42
+ PUSH=$(echo "$SCORES_LINE" | grep -oE 'push=[0-9]+' | cut -d= -f2) || true
43
+ RELEASE=$(echo "$SCORES_LINE" | grep -oE 'release=[0-9]+' | cut -d= -f2) || true
44
+
45
+ [ -n "$COMMIT" ] && printf '%s' "$COMMIT" > "${RDIR}/commit"
46
+ [ -n "$PUSH" ] && printf '%s' "$PUSH" > "${RDIR}/push"
47
+ [ -n "$RELEASE" ] && printf '%s' "$RELEASE" > "${RDIR}/release"
48
+ fi
49
+
50
+ # Parse RISK_BYPASS: reducing|incident
51
+ BYPASS_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_BYPASS:' | tail -1) || true
52
+ if [ -n "$BYPASS_LINE" ]; then
53
+ BYPASS_TYPE=$(echo "$BYPASS_LINE" | sed 's/^RISK_BYPASS:[[:space:]]*//' | tr -d '[:space:]')
54
+ case "$BYPASS_TYPE" in
55
+ reducing)
56
+ touch "${RDIR}/reducing-commit"
57
+ touch "${RDIR}/reducing-push"
58
+ touch "${RDIR}/reducing-release"
59
+ ;;
60
+ incident)
61
+ touch "${RDIR}/incident-release"
62
+ ;;
63
+ esac
64
+ fi
65
+
66
+ # Refresh pipeline state hash so drift detection matches scoring time
67
+ CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
68
+ if [ -n "$CURRENT_HASH" ]; then
69
+ echo "$CURRENT_HASH" > "${RDIR}/state-hash"
70
+ fi
71
+
72
+ # Save report to .risk-reports/
73
+ REPORT_DIR=".risk-reports"
74
+ mkdir -p "$REPORT_DIR"
75
+ TIMESTAMP=$(date -u +%Y-%m-%dT%H-%M-%S)
76
+ echo "$AGENT_OUTPUT" > "${REPORT_DIR}/${TIMESTAMP}-commit.md"
77
+ fi
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Plan scorer: write plan-reviewed marker on PASS
81
+ # ---------------------------------------------------------------------------
82
+ if echo "$SUBAGENT" | grep -qE 'risk-scorer-plan'; then
83
+ VERDICT_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_VERDICT:' | tail -1) || true
84
+ VERDICT=$(echo "$VERDICT_LINE" | sed 's/^RISK_VERDICT:[[:space:]]*//' | tr -d '[:space:]')
85
+ case "$VERDICT" in
86
+ PASS) touch "${RDIR}/plan-reviewed" ;;
87
+ FAIL) ;; # Do NOT create marker — plan must be revised
88
+ *) ;; # Unknown verdict — fail closed
89
+ esac
90
+
91
+ # Refresh pipeline state hash
92
+ CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
93
+ if [ -n "$CURRENT_HASH" ]; then
94
+ echo "$CURRENT_HASH" > "${RDIR}/state-hash"
95
+ fi
96
+ fi
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # WIP scorer: write wip-reviewed marker (unblocks next edit)
100
+ # ---------------------------------------------------------------------------
101
+ if echo "$SUBAGENT" | grep -qE 'risk-scorer-wip'; then
102
+ # WIP assessment was done — unblock next edit regardless of CONTINUE/PAUSE
103
+ # (PAUSE is advisory guidance to the user, not a hard gate)
104
+ touch "${RDIR}/wip-reviewed"
105
+ fi
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Policy scorer: write policy-reviewed marker on PASS
109
+ # ---------------------------------------------------------------------------
110
+ if echo "$SUBAGENT" | grep -qE 'risk-scorer-policy'; then
111
+ VERDICT_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_VERDICT:' | tail -1) || true
112
+ VERDICT=$(echo "$VERDICT_LINE" | sed 's/^RISK_VERDICT:[[:space:]]*//' | tr -d '[:space:]')
113
+ case "$VERDICT" in
114
+ PASS) touch "${RDIR}/policy-reviewed" ;;
115
+ FAIL) ;; # Do NOT create marker — policy must be revised
116
+ *) ;; # Unknown verdict — fail closed
117
+ esac
118
+ fi
119
+
120
+ exit 0
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: Denies ExitPlanMode until risk-scorer has reviewed
3
+ # the plan and given PASS. Mirrors architect-plan-enforce.sh pattern.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
9
+ _enable_err_trap
10
+
11
+ _parse_input
12
+
13
+ SESSION_ID=$(_get_session_id)
14
+ [ -n "$SESSION_ID" ] || exit 0
15
+
16
+ # Check for risk plan review marker
17
+ MARKER="$(_risk_dir "$SESSION_ID")/plan-reviewed"
18
+ if [ -f "$MARKER" ]; then
19
+ exit 0
20
+ fi
21
+
22
+ cat <<'EOF'
23
+ {
24
+ "hookSpecificOutput": {
25
+ "hookEventName": "PreToolUse",
26
+ "permissionDecision": "deny",
27
+ "permissionDecisionReason": "BLOCKED: Risk-scorer must review the plan before exiting plan mode. Delegate to risk-scorer-plan (subagent_type: 'risk-scorer-plan') to review the plan file for risk, including projected release risk."
28
+ }
29
+ }
30
+ EOF
31
+ exit 0
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Stop hook: Clears risk score temp files on session end.
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
6
+
7
+ _parse_input
8
+
9
+ SESSION_ID=$(_get_session_id)
10
+
11
+ if [ -n "$SESSION_ID" ]; then
12
+ # Remove the entire session-scoped directory
13
+ RDIR="${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}"
14
+ rm -rf "$RDIR"
15
+ fi
16
+
17
+ exit 0
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+ # UserPromptSubmit hook: No-op.
3
+ # Risk scoring is triggered by Edit/Write (WIP nudge gate) and gated actions
4
+ # (commit/push/release gates). This hook is retained only for the session
5
+ # marker lifecycle — creating the WIP marker so the first edit isn't blocked.
6
+
7
+ set -euo pipefail
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
11
+
12
+ _parse_input
13
+
14
+ SESSION_ID=$(_get_session_id)
15
+ [ -n "$SESSION_ID" ] || exit 0
16
+
17
+ # Create WIP marker so first edit of the session isn't blocked
18
+ RDIR=$(_risk_dir "$SESSION_ID")
19
+ WIP_MARKER="${RDIR}/wip-reviewed"
20
+ if [ ! -f "$WIP_MARKER" ]; then
21
+ touch "$WIP_MARKER"
22
+ fi
23
+
24
+ # Rotate old risk reports (keep last 7 days)
25
+ if [ -d ".risk-reports" ]; then
26
+ find .risk-reports -name '*.md' -mtime +7 -delete 2>/dev/null || true
27
+ fi
28
+
29
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: Blocks Edit/Write if content contains secret patterns.
3
+ # Mitigates WR-R2 (Secret leakage risk).
4
+
5
+ set -euo pipefail
6
+
7
+ INPUT=$(cat)
8
+
9
+ # Extract the content being written — check both "new_string" (Edit) and "content" (Write)
10
+ CONTENT=$(echo "$INPUT" | python3 -c "
11
+ import sys, json
12
+ try:
13
+ data = json.load(sys.stdin)
14
+ tool_input = data.get('tool_input', {})
15
+ text = tool_input.get('new_string', '') + tool_input.get('content', '')
16
+ print(text)
17
+ except:
18
+ print('')
19
+ " 2>/dev/null || echo "")
20
+
21
+ if [ -z "$CONTENT" ]; then
22
+ exit 0
23
+ fi
24
+
25
+ # Check for secret patterns
26
+ MATCHED=""
27
+
28
+ # AWS access keys
29
+ if echo "$CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
30
+ MATCHED="AWS access key"
31
+ fi
32
+
33
+ # Private keys
34
+ if echo "$CONTENT" | grep -qE 'BEGIN[[:space:]]+(RSA|DSA|EC|OPENSSH|PGP)?[[:space:]]*PRIVATE KEY'; then
35
+ MATCHED="private key"
36
+ fi
37
+
38
+ # GitHub tokens
39
+ if echo "$CONTENT" | grep -qE 'gh[pousr]_[A-Za-z0-9_]{36,}'; then
40
+ MATCHED="GitHub token"
41
+ fi
42
+
43
+ # Generic API key/secret/token assignments with actual values (not variable references)
44
+ if echo "$CONTENT" | grep -qEi '(api_key|api_secret|auth_key|auth_token|secret_key)[[:space:]]*[=:][[:space:]]*["\x27][A-Za-z0-9+/=_-]{16,}'; then
45
+ MATCHED="API key/secret assignment"
46
+ fi
47
+
48
+ # Cloudflare auth key pattern (specific to this project's legacy scripts)
49
+ if echo "$CONTENT" | grep -qE 'X-Auth-Key:[[:space:]]*[A-Za-z0-9]{30,}'; then
50
+ MATCHED="Cloudflare auth key"
51
+ fi
52
+
53
+ # Netlify auth token
54
+ if echo "$CONTENT" | grep -qE 'NETLIFY_AUTH_TOKEN[[:space:]]*[=:][[:space:]]*["\x27][A-Za-z0-9_-]{16,}'; then
55
+ MATCHED="Netlify auth token"
56
+ fi
57
+
58
+ if [ -n "$MATCHED" ]; then
59
+ cat <<EOF
60
+ {
61
+ "hookSpecificOutput": {
62
+ "hookEventName": "PreToolUse",
63
+ "permissionDecision": "deny",
64
+ "permissionDecisionReason": "BLOCKED (WR-R2): Detected probable $MATCHED in file content. Do not write secrets to files — use environment variables or CI secrets instead."
65
+ }
66
+ }
67
+ EOF
68
+ exit 0
69
+ fi
70
+
71
+ # No secrets detected — allow
72
+ exit 0
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bats
2
+ # Tests for .claude/hooks/lib/risk-gate.sh
3
+
4
+ setup() {
5
+ HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
6
+ source "$HOOKS_DIR/lib/gate-helpers.sh"
7
+ source "$HOOKS_DIR/lib/risk-gate.sh"
8
+
9
+ TEST_SESSION="bats-test-$$-${BATS_TEST_NUMBER}"
10
+ RDIR=$(_risk_dir "$TEST_SESSION")
11
+ SCORE_FILE="${RDIR}/commit"
12
+ HASH_FILE="${RDIR}/state-hash"
13
+
14
+ export RISK_TTL=5
15
+ rm -f "$SCORE_FILE" "$HASH_FILE"
16
+ }
17
+
18
+ teardown() {
19
+ rm -rf "${TMPDIR:-/tmp}/claude-risk-${TEST_SESSION}"
20
+ }
21
+
22
+ # Helper: call check_risk_gate directly (not via run) so RISK_GATE_REASON is visible
23
+ assert_gate_denies() {
24
+ local session="$1" action="$2" expected_reason="$3"
25
+ RISK_GATE_REASON=""
26
+ if check_risk_gate "$session" "$action"; then
27
+ echo "Expected gate to deny but it allowed"
28
+ return 1
29
+ fi
30
+ if [[ "$RISK_GATE_REASON" != *"$expected_reason"* ]]; then
31
+ echo "Expected reason to contain '$expected_reason' but got: $RISK_GATE_REASON"
32
+ return 1
33
+ fi
34
+ }
35
+
36
+ assert_gate_allows() {
37
+ local session="$1" action="$2"
38
+ if ! check_risk_gate "$session" "$action"; then
39
+ echo "Expected gate to allow but it denied: $RISK_GATE_REASON"
40
+ return 1
41
+ fi
42
+ }
43
+
44
+ @test "missing score file denies" {
45
+ assert_gate_denies "$TEST_SESSION" "commit" "No commit risk score found"
46
+ }
47
+
48
+ @test "score file with PENDING denies (non-numeric)" {
49
+ printf 'PENDING' > "$SCORE_FILE"
50
+ assert_gate_denies "$TEST_SESSION" "commit" "invalid value"
51
+ }
52
+
53
+ @test "score 4 allows (below threshold)" {
54
+ printf '4' > "$SCORE_FILE"
55
+ assert_gate_allows "$TEST_SESSION" "commit"
56
+ }
57
+
58
+ @test "score 5 denies (at threshold)" {
59
+ printf '5' > "$SCORE_FILE"
60
+ assert_gate_denies "$TEST_SESSION" "commit" "5/25"
61
+ }
62
+
63
+ @test "score 8 denies (above threshold)" {
64
+ printf '8' > "$SCORE_FILE"
65
+ assert_gate_denies "$TEST_SESSION" "commit" "8/25"
66
+ }
67
+
68
+ @test "score 1 allows (very low)" {
69
+ printf '1' > "$SCORE_FILE"
70
+ assert_gate_allows "$TEST_SESSION" "commit"
71
+ }
72
+
73
+ @test "expired score file denies" {
74
+ printf '3' > "$SCORE_FILE"
75
+ # Backdate mtime by 10 seconds (TTL is 5)
76
+ touch -t "$(date -v-10S +%Y%m%d%H%M.%S 2>/dev/null || date -d '10 seconds ago' +%Y%m%d%H%M.%S 2>/dev/null)" "$SCORE_FILE"
77
+ assert_gate_denies "$TEST_SESSION" "commit" "expired"
78
+ }
79
+
80
+ @test "fresh score file allows" {
81
+ printf '3' > "$SCORE_FILE"
82
+ touch "$SCORE_FILE"
83
+ assert_gate_allows "$TEST_SESSION" "commit"
84
+ }
85
+
86
+ @test "drift detection: hash mismatch denies" {
87
+ printf '3' > "$SCORE_FILE"
88
+ touch "$SCORE_FILE"
89
+ echo "oldhash123" > "$HASH_FILE"
90
+ assert_gate_denies "$TEST_SESSION" "commit" "drift"
91
+ }
92
+
93
+ @test "no hash file skips drift check (backwards compat)" {
94
+ printf '3' > "$SCORE_FILE"
95
+ touch "$SCORE_FILE"
96
+ rm -f "$HASH_FILE"
97
+ assert_gate_allows "$TEST_SESSION" "commit"
98
+ }
99
+
100
+ @test "risk_gate_deny outputs valid JSON" {
101
+ run risk_gate_deny "Test reason"
102
+ [ "$status" -eq 0 ]
103
+ echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)" 2>/dev/null
104
+ [[ "$output" == *"permissionDecision"* ]]
105
+ [[ "$output" == *"deny"* ]]
106
+ [[ "$output" == *"Test reason"* ]]
107
+ }
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: Blocks Edit/Write on non-doc files until WIP risk
3
+ # assessment has been completed by the risk-scorer in WIP nudge mode.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
9
+ _enable_err_trap
10
+
11
+ _parse_input
12
+
13
+ TOOL_NAME=$(_get_tool_name)
14
+ case "$TOOL_NAME" in
15
+ Edit|Write) ;;
16
+ *) exit 0 ;;
17
+ esac
18
+
19
+ FILE_PATH=$(_get_file_path)
20
+ [ -n "$FILE_PATH" ] || exit 0
21
+
22
+ # Skip doc/governance files
23
+ if _is_doc_file "$FILE_PATH"; then
24
+ exit 0
25
+ fi
26
+
27
+ SESSION_ID=$(_get_session_id)
28
+ [ -n "$SESSION_ID" ] || exit 0
29
+
30
+ MARKER="$(_risk_dir "$SESSION_ID")/wip-reviewed"
31
+ if [ -f "$MARKER" ]; then
32
+ exit 0
33
+ fi
34
+
35
+ cat <<'EOF'
36
+ {
37
+ "hookSpecificOutput": {
38
+ "hookEventName": "PreToolUse",
39
+ "permissionDecision": "deny",
40
+ "permissionDecisionReason": "WIP risk assessment required. Delegate to risk-scorer-wip (subagent_type: 'risk-scorer-wip') to assess cumulative pipeline risk for changes so far."
41
+ }
42
+ }
43
+ EOF
44
+ exit 0
@@ -0,0 +1,32 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: Manages the WIP-reviewed marker.
3
+ # - After Edit/Write on non-doc files: clears the marker (blocks next edit)
4
+ # - After Agent (risk-scorer) completion: creates the marker (unblocks next edit)
5
+
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
10
+ _enable_err_trap
11
+
12
+ _parse_input
13
+
14
+ TOOL_NAME=$(_get_tool_name)
15
+ SESSION_ID=$(_get_session_id)
16
+ [ -n "$SESSION_ID" ] || exit 0
17
+
18
+ MARKER="$(_risk_dir "$SESSION_ID")/wip-reviewed"
19
+
20
+ case "$TOOL_NAME" in
21
+ Edit|Write)
22
+ FILE_PATH=$(_get_file_path)
23
+ [ -n "$FILE_PATH" ] || exit 0
24
+
25
+ if ! _is_doc_file "$FILE_PATH"; then
26
+ rm -f "$MARKER"
27
+ fi
28
+ ;;
29
+ # Agent case handled by risk-score-mark.sh
30
+ esac
31
+
32
+ exit 0
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@windyroad/risk-scorer",
3
+ "version": "0.2.0",
4
+ "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
+ "bin": {
6
+ "windyroad-risk-scorer": "./bin/install.mjs"
7
+ },
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/windyroad/agent-plugins.git",
13
+ "directory": "packages/risk-scorer"
14
+ },
15
+ "keywords": [
16
+ "claude-code",
17
+ "claude-code-plugin",
18
+ "ai-agent",
19
+ "ai-coding"
20
+ ],
21
+ "files": [
22
+ "bin/",
23
+ "agents/",
24
+ "hooks/",
25
+ "skills/",
26
+ ".claude-plugin/"
27
+ ]
28
+ }