@windyroad/risk-scorer 0.1.3 → 0.1.4-preview.27
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/README.md +76 -0
- package/hooks/git-push-gate.sh +10 -1
- package/hooks/risk-policy-enforce-edit.sh +1 -1
- package/hooks/test/git-push-gate.bats +82 -0
- package/hooks/test/risk-score-mark.bats +39 -0
- package/hooks/wip-risk-mark.sh +3 -11
- package/package.json +1 -1
- package/skills/{wr:risk-policy → update-policy}/SKILL.md +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @windyroad/risk-scorer
|
|
2
|
+
|
|
3
|
+
**Pipeline risk scoring, commit/push gates, and secret leak detection for Claude Code.** Scores every change for risk and blocks high-risk commits and pushes before they happen.
|
|
4
|
+
|
|
5
|
+
Part of [Windy Road Agent Plugins](../../README.md).
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
The risk-scorer plugin brings ISO 31000-aligned risk management to your AI coding workflow. It:
|
|
10
|
+
|
|
11
|
+
1. **Scores risk** on every edit, assessing cumulative pipeline risk as changes build up
|
|
12
|
+
2. **Gates commits** -- blocks `git commit` when cumulative risk exceeds your policy threshold
|
|
13
|
+
3. **Gates pushes** -- blocks `git push` for high-risk changesets (use `npm run push:watch` instead)
|
|
14
|
+
4. **Detects secrets** -- scans edits for API keys, tokens, passwords, and other credentials before they're written
|
|
15
|
+
5. **Reviews plans** -- scores implementation plans for risk before you start building
|
|
16
|
+
|
|
17
|
+
All thresholds are configurable through your project's `RISK-POLICY.md`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @windyroad/risk-scorer
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Restart Claude Code after installing.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
The plugin works automatically once installed. On first run in a project without a risk policy, it blocks edits and directs you to generate one:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/wr-risk-scorer:update-policy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This creates a `RISK-POLICY.md` tailored to your project, defining impact levels, likelihood scales, risk appetite, and the risk matrix -- all aligned to ISO 31000.
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
| Hook | Trigger | What it does |
|
|
40
|
+
|------|---------|-------------|
|
|
41
|
+
| `risk-score.sh` | Every prompt | Injects risk scoring context |
|
|
42
|
+
| `secret-leak-gate.sh` | Edit or Write | Blocks writes containing secrets |
|
|
43
|
+
| `wip-risk-gate.sh` | Edit or Write | Blocks edits if WIP risk hasn't been assessed |
|
|
44
|
+
| `risk-policy-enforce-edit.sh` | Edit or Write | Blocks edits if no `RISK-POLICY.md` exists |
|
|
45
|
+
| `git-push-gate.sh` | Bash (git push) | Blocks direct `git push`; requires `npm run push:watch` |
|
|
46
|
+
| `risk-score-commit-gate.sh` | Bash (git commit) | Blocks commits when risk exceeds threshold |
|
|
47
|
+
| `risk-score-plan-enforce.sh` | ExitPlanMode | Ensures plans are risk-scored before execution |
|
|
48
|
+
| `plan-risk-guidance.sh` | EnterPlanMode | Injects risk guidance into plan mode |
|
|
49
|
+
| `wip-risk-mark.sh` | After edit | Records WIP risk assessment |
|
|
50
|
+
| `risk-score-mark.sh` | Agent completes | Marks risk review as done |
|
|
51
|
+
| `risk-hash-refresh.sh` | After Bash | Refreshes content hashes |
|
|
52
|
+
| `risk-score-reset.sh` | Session end | Cleans up risk markers |
|
|
53
|
+
| `risk-policy-reset-marker.sh` | Session end | Cleans up policy markers |
|
|
54
|
+
|
|
55
|
+
## Agents
|
|
56
|
+
|
|
57
|
+
The plugin includes five specialised agents:
|
|
58
|
+
|
|
59
|
+
| Agent | Purpose |
|
|
60
|
+
|-------|---------|
|
|
61
|
+
| `wr-risk-scorer:agent` | Routes to the appropriate mode-specific agent |
|
|
62
|
+
| `wr-risk-scorer:wip` | Assesses cumulative risk after each edit |
|
|
63
|
+
| `wr-risk-scorer:pipeline` | Scores pipeline actions (commit, push, release) |
|
|
64
|
+
| `wr-risk-scorer:plan` | Reviews implementation plans for risk |
|
|
65
|
+
| `wr-risk-scorer:policy` | Validates `RISK-POLICY.md` for ISO 31000 compliance |
|
|
66
|
+
|
|
67
|
+
## Updating and Uninstalling
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx @windyroad/risk-scorer --update
|
|
71
|
+
npx @windyroad/risk-scorer --uninstall
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Licence
|
|
75
|
+
|
|
76
|
+
[MIT](../../LICENSE)
|
package/hooks/git-push-gate.sh
CHANGED
|
@@ -110,7 +110,16 @@ fi
|
|
|
110
110
|
|
|
111
111
|
# Match gh pr merge. Should go via npm run release:watch instead.
|
|
112
112
|
if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*gh pr merge(\s|$)'; then
|
|
113
|
-
|
|
113
|
+
# Check if the project has a release:watch script
|
|
114
|
+
if [ -f "package.json" ] && python3 -c "
|
|
115
|
+
import json, sys
|
|
116
|
+
pkg = json.load(open('package.json'))
|
|
117
|
+
sys.exit(0 if 'release:watch' in pkg.get('scripts', {}) else 1)
|
|
118
|
+
" 2>/dev/null; then
|
|
119
|
+
risk_gate_deny "Use \`npm run release:watch\` instead of \`gh pr merge\`. It merges the release PR, watches the publish pipeline, and surfaces the production URL when live -- or tells you what failed and how to fix it."
|
|
120
|
+
else
|
|
121
|
+
risk_gate_deny "Direct \`gh pr merge\` is blocked (no release:watch script found). Create a release:watch npm script that: (1) finds and merges the release PR with \`gh pr merge\`, (2) waits for the CI workflow with \`gh run list\`, and (3) watches it with \`gh run watch --exit-status\`. Then run \`npm run release:watch\` to release."
|
|
122
|
+
fi
|
|
114
123
|
exit 0
|
|
115
124
|
fi
|
|
116
125
|
|
|
@@ -35,7 +35,7 @@ cat <<'EOF'
|
|
|
35
35
|
"hookSpecificOutput": {
|
|
36
36
|
"hookEventName": "PreToolUse",
|
|
37
37
|
"permissionDecision": "deny",
|
|
38
|
-
"permissionDecisionReason": "BLOCKED: Cannot edit RISK-POLICY.md directly. Run
|
|
38
|
+
"permissionDecisionReason": "BLOCKED: Cannot edit RISK-POLICY.md directly. Run /wr-risk-scorer:update-policy 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: \"wr-risk-scorer:update-policy\"."
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
EOF
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# Tests for git-push-gate.sh — gh pr merge block and release:watch guidance
|
|
3
|
+
|
|
4
|
+
setup() {
|
|
5
|
+
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
6
|
+
HOOK="$HOOKS_DIR/git-push-gate.sh"
|
|
7
|
+
|
|
8
|
+
TEST_SESSION="bats-push-gate-$$-${BATS_TEST_NUMBER}"
|
|
9
|
+
# Ensure a clean risk dir
|
|
10
|
+
RDIR="${TMPDIR:-/tmp}/claude-risk-${TEST_SESSION}"
|
|
11
|
+
rm -rf "$RDIR"
|
|
12
|
+
mkdir -p "$RDIR"
|
|
13
|
+
|
|
14
|
+
# Create a temp project dir for package.json detection
|
|
15
|
+
TEST_PROJECT_DIR="$(mktemp -d)"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
teardown() {
|
|
19
|
+
rm -rf "$RDIR"
|
|
20
|
+
rm -rf "$TEST_PROJECT_DIR"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Helper: build a PreToolUse Bash input with a given command
|
|
24
|
+
build_input() {
|
|
25
|
+
local cmd="$1"
|
|
26
|
+
cat <<ENDJSON
|
|
27
|
+
{
|
|
28
|
+
"session_id": "$TEST_SESSION",
|
|
29
|
+
"tool_name": "Bash",
|
|
30
|
+
"tool_input": {
|
|
31
|
+
"command": "$cmd"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
ENDJSON
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "gh pr merge is blocked with release:watch guidance when script exists" {
|
|
38
|
+
# Create a package.json with release:watch
|
|
39
|
+
cat > "$TEST_PROJECT_DIR/package.json" <<'PKG'
|
|
40
|
+
{ "scripts": { "release:watch": "bash scripts/release-watch.sh" } }
|
|
41
|
+
PKG
|
|
42
|
+
|
|
43
|
+
INPUT=$(build_input "gh pr merge 4 --merge")
|
|
44
|
+
run bash -c "cd '$TEST_PROJECT_DIR' && echo '$INPUT' | '$HOOK'"
|
|
45
|
+
[ "$status" -eq 0 ]
|
|
46
|
+
[[ "$output" == *"permissionDecision"* ]]
|
|
47
|
+
[[ "$output" == *"deny"* ]]
|
|
48
|
+
[[ "$output" == *"release:watch"* ]]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "gh pr merge tells agent to create release:watch when script missing" {
|
|
52
|
+
# Create a package.json WITHOUT release:watch
|
|
53
|
+
cat > "$TEST_PROJECT_DIR/package.json" <<'PKG'
|
|
54
|
+
{ "scripts": { "test": "echo test" } }
|
|
55
|
+
PKG
|
|
56
|
+
|
|
57
|
+
INPUT=$(build_input "gh pr merge 4 --merge")
|
|
58
|
+
run bash -c "cd '$TEST_PROJECT_DIR' && echo '$INPUT' | '$HOOK'"
|
|
59
|
+
[ "$status" -eq 0 ]
|
|
60
|
+
[[ "$output" == *"permissionDecision"* ]]
|
|
61
|
+
[[ "$output" == *"deny"* ]]
|
|
62
|
+
# Should tell agent to create the script
|
|
63
|
+
[[ "$output" == *"no release:watch script"* ]]
|
|
64
|
+
[[ "$output" == *"gh pr merge"* ]]
|
|
65
|
+
[[ "$output" == *"gh run watch"* ]]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "gh pr merge tells agent to create release:watch when no package.json" {
|
|
69
|
+
local empty_dir="$(mktemp -d)"
|
|
70
|
+
|
|
71
|
+
INPUT=$(build_input "gh pr merge 4 --merge")
|
|
72
|
+
run bash -c "cd '$empty_dir' && echo '$INPUT' | '$HOOK'"
|
|
73
|
+
[ "$status" -eq 0 ]
|
|
74
|
+
[[ "$output" == *"permissionDecision"* ]]
|
|
75
|
+
[[ "$output" == *"deny"* ]]
|
|
76
|
+
# Should tell agent to create the script
|
|
77
|
+
[[ "$output" == *"no release:watch script"* ]]
|
|
78
|
+
[[ "$output" == *"gh pr merge"* ]]
|
|
79
|
+
[[ "$output" == *"gh run watch"* ]]
|
|
80
|
+
|
|
81
|
+
rm -rf "$empty_dir"
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for risk-score-mark.sh subagent pattern matching
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@test "pattern matches colon-style: wr-risk-scorer:pipeline" {
|
|
10
|
+
echo "wr-risk-scorer:pipeline" | grep -qE 'risk-scorer.pipeline'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "pattern matches colon-style: wr-risk-scorer:plan" {
|
|
14
|
+
echo "wr-risk-scorer:plan" | grep -qE 'risk-scorer.plan'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@test "pattern matches colon-style: wr-risk-scorer:wip" {
|
|
18
|
+
echo "wr-risk-scorer:wip" | grep -qE 'risk-scorer.wip'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@test "pattern matches colon-style: wr-risk-scorer:policy" {
|
|
22
|
+
echo "wr-risk-scorer:policy" | grep -qE 'risk-scorer.policy'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@test "case guard matches wr-risk-scorer:pipeline" {
|
|
26
|
+
SUBAGENT="wr-risk-scorer:pipeline"
|
|
27
|
+
case "$SUBAGENT" in
|
|
28
|
+
*risk-scorer*) true ;;
|
|
29
|
+
*) false ;;
|
|
30
|
+
esac
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@test "case guard does NOT match unrelated agent" {
|
|
34
|
+
SUBAGENT="wr-architect:agent"
|
|
35
|
+
case "$SUBAGENT" in
|
|
36
|
+
*risk-scorer*) false ;;
|
|
37
|
+
*) true ;;
|
|
38
|
+
esac
|
|
39
|
+
}
|
package/hooks/wip-risk-mark.sh
CHANGED
|
@@ -17,16 +17,8 @@ SESSION_ID=$(_get_session_id)
|
|
|
17
17
|
|
|
18
18
|
MARKER="$(_risk_dir "$SESSION_ID")/wip-reviewed"
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
20
|
+
# WIP marker persists after assessment — allows multiple edits.
|
|
21
|
+
# Cleared on session end by risk-score-reset.sh (Stop hook).
|
|
22
|
+
# Agent case (marker creation) handled by risk-score-mark.sh.
|
|
31
23
|
|
|
32
24
|
exit 0
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: wr
|
|
2
|
+
name: wr-risk-scorer:update-policy
|
|
3
3
|
description: Create or update the project's RISK-POLICY.md per ISO 31000 and the risk-scorer agent. Examines the project to derive business-specific impact levels.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent
|
|
5
5
|
---
|