@qball-inc/the-bulwark 1.2.0 → 1.3.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/.claude-plugin/plugin.json +50 -42
- package/CHANGELOG.md +102 -30
- package/CONTRIBUTING.md +52 -0
- package/README.md +97 -328
- package/hooks/hooks.json +100 -88
- package/package.json +46 -46
- package/scripts/hooks/bulwark-permission-hook.sh +306 -0
- package/skills/anthropic-validator/SKILL.md +6 -0
- package/skills/anthropic-validator/references/skills-checklist.md +2 -1
- package/skills/anthropic-validator/references/skills-validation.md +2 -1
- package/skills/assertion-patterns/SKILL.md +3 -0
- package/skills/bug-magnet-data/SKILL.md +3 -0
- package/skills/bulwark-brainstorm/SKILL.md +8 -0
- package/skills/bulwark-research/SKILL.md +8 -0
- package/skills/bulwark-scaffold/SKILL.md +75 -2
- package/skills/bulwark-statusline/SKILL.md +3 -1
- package/skills/bulwark-verify/SKILL.md +9 -0
- package/skills/code-review/SKILL.md +72 -89
- package/skills/code-review/references/diagnostic-schema.md +119 -0
- package/skills/component-patterns/SKILL.md +3 -0
- package/skills/continuous-feedback/SKILL.md +9 -0
- package/skills/create-skill/SKILL.md +9 -0
- package/skills/create-subagent/SKILL.md +7 -0
- package/skills/fix-bug/SKILL.md +4 -0
- package/skills/governance-protocol/SKILL.md +1 -0
- package/skills/init/SKILL.md +6 -0
- package/skills/issue-debugging/SKILL.md +3 -0
- package/skills/mock-detection/SKILL.md +5 -0
- package/skills/pipeline-templates/SKILL.md +3 -0
- package/skills/plan-creation/SKILL.md +10 -0
- package/skills/plan-to-tasks/SKILL.md +8 -0
- package/skills/product-ideation/SKILL.md +6 -0
- package/skills/session-handoff/SKILL.md +4 -0
- package/skills/setup-lsp/SKILL.md +6 -0
- package/skills/spec-drift-check/SKILL.md +8 -5
- package/skills/subagent-output-templating/SKILL.md +2 -0
- package/skills/subagent-prompting/SKILL.md +2 -0
- package/skills/test-audit/SKILL.md +10 -0
- package/skills/test-classification/SKILL.md +5 -0
- package/skills/test-fixture-creation/SKILL.md +6 -0
package/hooks/hooks.json
CHANGED
|
@@ -1,88 +1,100 @@
|
|
|
1
|
-
{
|
|
2
|
-
"description": "The Bulwark quality enforcement hooks - Defense-in-Depth OUTER RING",
|
|
3
|
-
"hooks": {
|
|
4
|
-
"
|
|
5
|
-
{
|
|
6
|
-
"matcher": "
|
|
7
|
-
"hooks": [
|
|
8
|
-
{
|
|
9
|
-
"type": "command",
|
|
10
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/
|
|
11
|
-
"timeout":
|
|
12
|
-
}
|
|
13
|
-
]
|
|
14
|
-
}
|
|
15
|
-
],
|
|
16
|
-
"
|
|
17
|
-
{
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"description": "The Bulwark quality enforcement hooks - Defense-in-Depth OUTER RING",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Read|Edit|Bash",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/bulwark-permission-hook.sh",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"PostToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/enforce-quality.sh",
|
|
23
|
+
"timeout": 60
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"SubagentStart": [
|
|
29
|
+
{
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/track-pipeline-start.sh",
|
|
34
|
+
"timeout": 30
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"SubagentStop": [
|
|
40
|
+
{
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/track-pipeline-stop.sh",
|
|
45
|
+
"timeout": 30
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"Stop": [
|
|
51
|
+
{
|
|
52
|
+
"hooks": [
|
|
53
|
+
{
|
|
54
|
+
"type": "command",
|
|
55
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/suggest-pipeline-stop.sh",
|
|
56
|
+
"timeout": 30
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"SessionStart": [
|
|
62
|
+
{
|
|
63
|
+
"hooks": [
|
|
64
|
+
{
|
|
65
|
+
"type": "command",
|
|
66
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/inject-protocol.sh",
|
|
67
|
+
"timeout": 5
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"hooks": [
|
|
73
|
+
{
|
|
74
|
+
"type": "command",
|
|
75
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/cleanup-stale.sh",
|
|
76
|
+
"timeout": 30
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"hooks": [
|
|
82
|
+
{
|
|
83
|
+
"type": "command",
|
|
84
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/cleanup-review-registry.sh",
|
|
85
|
+
"timeout": 5
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"hooks": [
|
|
91
|
+
{
|
|
92
|
+
"type": "command",
|
|
93
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/check-template-drift.sh",
|
|
94
|
+
"timeout": 5
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@qball-inc/the-bulwark",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Full-lifecycle SDLC guardrailing framework for Claude Code — from product ideation and planning through implementation, code review, and test validation. Enterprise-grade skills and agents for AI-human peer collaboration.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Ashay Kubal <https://ashaykubal.com>",
|
|
7
|
-
"homepage": "https://github.com/QBall-Inc",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/QBall-Inc/the-bulwark.git"
|
|
11
|
-
},
|
|
12
|
-
"keywords": [
|
|
13
|
-
"claude-code",
|
|
14
|
-
"claude-code-plugin",
|
|
15
|
-
"sdlc",
|
|
16
|
-
"quality-enforcement",
|
|
17
|
-
"code-review",
|
|
18
|
-
"testing",
|
|
19
|
-
"governance",
|
|
20
|
-
"ideation",
|
|
21
|
-
"product-ideation",
|
|
22
|
-
"product-management",
|
|
23
|
-
"market-research",
|
|
24
|
-
"competitive-research",
|
|
25
|
-
"brainstorming",
|
|
26
|
-
"planning",
|
|
27
|
-
"plan-creation",
|
|
28
|
-
"agent-design",
|
|
29
|
-
"skill-design",
|
|
30
|
-
"test-audit",
|
|
31
|
-
"statusline",
|
|
32
|
-
"agent-teams"
|
|
33
|
-
],
|
|
34
|
-
"scripts": {
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"lint": "eslint . --ext .ts"
|
|
37
|
-
},
|
|
38
|
-
"devDependencies": {
|
|
39
|
-
"@types/bun": "^1.3.13",
|
|
40
|
-
"@types/jest": "^29.5.0",
|
|
41
|
-
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
42
|
-
"@typescript-eslint/parser": "^6.21.0",
|
|
43
|
-
"eslint": "^8.56.0",
|
|
44
|
-
"typescript": "^5.3.0"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@qball-inc/the-bulwark",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Full-lifecycle SDLC guardrailing framework for Claude Code — from product ideation and planning through implementation, code review, and test validation. Enterprise-grade skills and agents for AI-human peer collaboration.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ashay Kubal <https://ashaykubal.com>",
|
|
7
|
+
"homepage": "https://github.com/QBall-Inc",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/QBall-Inc/the-bulwark.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude-code",
|
|
14
|
+
"claude-code-plugin",
|
|
15
|
+
"sdlc",
|
|
16
|
+
"quality-enforcement",
|
|
17
|
+
"code-review",
|
|
18
|
+
"testing",
|
|
19
|
+
"governance",
|
|
20
|
+
"ideation",
|
|
21
|
+
"product-ideation",
|
|
22
|
+
"product-management",
|
|
23
|
+
"market-research",
|
|
24
|
+
"competitive-research",
|
|
25
|
+
"brainstorming",
|
|
26
|
+
"planning",
|
|
27
|
+
"plan-creation",
|
|
28
|
+
"agent-design",
|
|
29
|
+
"skill-design",
|
|
30
|
+
"test-audit",
|
|
31
|
+
"statusline",
|
|
32
|
+
"agent-teams"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"lint": "eslint . --ext .ts"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "^1.3.13",
|
|
40
|
+
"@types/jest": "^29.5.0",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
42
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
43
|
+
"eslint": "^8.56.0",
|
|
44
|
+
"typescript": "^5.3.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# bulwark-permission-hook.sh - PreToolUse permission-bypass for Bulwark bundled assets
|
|
3
|
+
#
|
|
4
|
+
# OPT-IN, DEFAULT OFF. Auto-approves Read/Edit/Bash operations whose target
|
|
5
|
+
# resolves INSIDE a Bulwark plugin root, and passes through (no opinion) for
|
|
6
|
+
# everything else. A scoped workaround for upstream CC permission-prompt bugs
|
|
7
|
+
# on plugin-bundled assets the user already trusted at install time.
|
|
8
|
+
#
|
|
9
|
+
# Design + safety model: docs/internal/p10.8-hook-design.md
|
|
10
|
+
# Brief: plans/task-briefs/P10.8-pretooluse-hook-permission-bypass.md
|
|
11
|
+
#
|
|
12
|
+
# Event: PreToolUse, matcher "Read|Edit|Bash"
|
|
13
|
+
# stdin: PreToolUse event JSON (.tool_name, .tool_input.file_path | .command)
|
|
14
|
+
# stdout: hookSpecificOutput JSON for allow/deny; EMPTY for pass-through (defer)
|
|
15
|
+
# exit: always 0 — decisions are expressed in JSON, never via exit 2
|
|
16
|
+
#
|
|
17
|
+
# Decision model (all on the CANONICAL path, never the literal string):
|
|
18
|
+
# - target canonically under a plugin root -> allow
|
|
19
|
+
# - target CLAIMS a plugin root but escapes it -> deny (anti-spoof)
|
|
20
|
+
# - anything else -> pass-through (defer)
|
|
21
|
+
# Bash is the highest-risk surface: only SIMPLE commands (no shell
|
|
22
|
+
# metacharacters) whose every path token sits under a plugin root are allowed.
|
|
23
|
+
# Write is intentionally NOT matched -> writes always keep CC's default prompt.
|
|
24
|
+
#
|
|
25
|
+
# Opt-in gate (default OFF), CONTEXT-AWARE so default-OFF never depends on CC
|
|
26
|
+
# exporting a userConfig default:
|
|
27
|
+
# - Plugin context ($CLAUDE_PLUGIN_ROOT set — installed plugin or --plugin-dir):
|
|
28
|
+
# require an EXPLICIT $CLAUDE_PLUGIN_OPTION_ENABLE_PERMISSION_BYPASS=true;
|
|
29
|
+
# unset / "false" / anything-else -> inert (fail-safe OFF). The plugin's
|
|
30
|
+
# userConfig.enable_permission_bypass (default false) drives this — and even
|
|
31
|
+
# if CC omits the var for an unchanged default, the hook stays OFF.
|
|
32
|
+
# - Project/scaffold context ($CLAUDE_PLUGIN_ROOT unset): the hook's presence
|
|
33
|
+
# in the project's .claude/settings.json IS the opt-in, so an UNSET var is
|
|
34
|
+
# active; an explicit "false" still disables it.
|
|
35
|
+
#
|
|
36
|
+
# Portability: prefers GNU `realpath -m`; falls back to a pure-bash lexical
|
|
37
|
+
# `..`/`.` normalizer (no filesystem access) so behavior is identical on
|
|
38
|
+
# WSL/Linux/macOS. Canonicalization is fail-safe: a path it cannot resolve
|
|
39
|
+
# to an absolute form never matches a root (so it is never auto-approved).
|
|
40
|
+
#
|
|
41
|
+
# Usage: invoked by Claude Code as a PreToolUse hook; not run directly.
|
|
42
|
+
|
|
43
|
+
set -euo pipefail
|
|
44
|
+
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
# Opt-in gate (default OFF) — context-aware (see header)
|
|
47
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
GATE="${CLAUDE_PLUGIN_OPTION_ENABLE_PERMISSION_BYPASS-__unset__}"
|
|
49
|
+
if [ -n "${CLAUDE_PLUGIN_ROOT-}" ]; then
|
|
50
|
+
# Plugin context: default-OFF must NOT depend on CC exporting the userConfig
|
|
51
|
+
# default, so require an EXPLICIT opt-in. unset/false/unknown -> inert.
|
|
52
|
+
case "$GATE" in
|
|
53
|
+
true|1|yes|on) : ;;
|
|
54
|
+
*) exit 0 ;;
|
|
55
|
+
esac
|
|
56
|
+
else
|
|
57
|
+
# Project/scaffold context: presence in .claude/settings.json IS the opt-in,
|
|
58
|
+
# so an UNSET gate is active; an explicit "false" still disables.
|
|
59
|
+
case "$GATE" in
|
|
60
|
+
__unset__|true|1|yes|on) : ;;
|
|
61
|
+
*) exit 0 ;;
|
|
62
|
+
esac
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
# Decision emitters
|
|
67
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
emit_allow() {
|
|
69
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"%s"}}\n' "$1"
|
|
70
|
+
exit 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
emit_deny() {
|
|
74
|
+
log_deny "$1" "${2:-}"
|
|
75
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$1"
|
|
76
|
+
exit 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Deny is rare and security-relevant -> log it (allows/pass-throughs are hot-path,
|
|
80
|
+
# not logged). Fully non-fatal; only appends if a logs/ dir already exists.
|
|
81
|
+
log_deny() {
|
|
82
|
+
local reason="$1" detail="$2"
|
|
83
|
+
local logdir="${CLAUDE_PROJECT_DIR:-.}/logs"
|
|
84
|
+
[ -d "$logdir" ] || return 0
|
|
85
|
+
# Sanitize the user-influenced detail before appending to the shared log:
|
|
86
|
+
# strip CR/LF (prevents log-line spoofing) and bound length (SEC-SUG-1).
|
|
87
|
+
detail="${detail//$'\n'/ }"
|
|
88
|
+
detail="${detail//$'\r'/ }"
|
|
89
|
+
detail="${detail:0:500}"
|
|
90
|
+
printf '[%s] PreToolUse bulwark-permission-hook: DENY %s (%s)\n' \
|
|
91
|
+
"$(date -Iseconds 2>/dev/null || echo now)" "$reason" "$detail" \
|
|
92
|
+
>> "$logdir/hooks.log" 2>/dev/null || true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
# Path expansion + canonicalization
|
|
97
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
# Expand a leading ~ and any literal $HOME / $CLAUDE_PLUGIN_ROOT tokens the
|
|
100
|
+
# Bash command string may carry unexpanded.
|
|
101
|
+
expand_path() {
|
|
102
|
+
local p="$1"
|
|
103
|
+
# Match a LITERAL leading tilde in the input and expand it ourselves; tildes
|
|
104
|
+
# must NOT shell-expand inside these case patterns (SC2088 here is intentional).
|
|
105
|
+
# shellcheck disable=SC2088
|
|
106
|
+
case "$p" in
|
|
107
|
+
"~") p="${HOME:-}" ;;
|
|
108
|
+
"~/"*) p="${HOME:-}/${p#"~/"}" ;;
|
|
109
|
+
esac
|
|
110
|
+
p="${p//\$\{CLAUDE_PLUGIN_ROOT\}/${CLAUDE_PLUGIN_ROOT:-}}"
|
|
111
|
+
p="${p//\$CLAUDE_PLUGIN_ROOT/${CLAUDE_PLUGIN_ROOT:-}}"
|
|
112
|
+
p="${p//\$\{HOME\}/${HOME:-}}"
|
|
113
|
+
p="${p//\$HOME/${HOME:-}}"
|
|
114
|
+
printf '%s' "$p"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Pure-bash lexical resolution of . and .. — no filesystem access, portable.
|
|
118
|
+
# Only normalizes ABSOLUTE paths; a relative path is returned unchanged so it
|
|
119
|
+
# can never match an absolute root (fail-safe).
|
|
120
|
+
lexical_normalize() {
|
|
121
|
+
local path="$1"
|
|
122
|
+
case "$path" in
|
|
123
|
+
/*) ;;
|
|
124
|
+
*) printf '%s' "$path"; return 0 ;;
|
|
125
|
+
esac
|
|
126
|
+
local IFS='/' seg
|
|
127
|
+
local -a parts=() out=()
|
|
128
|
+
read -ra parts <<< "$path" || true
|
|
129
|
+
for seg in "${parts[@]}"; do
|
|
130
|
+
case "$seg" in
|
|
131
|
+
''|'.') ;; # drop empty + current-dir
|
|
132
|
+
'..') [ "${#out[@]}" -gt 0 ] && unset 'out[${#out[@]}-1]' && out=("${out[@]}") ;;
|
|
133
|
+
*) out+=("$seg") ;;
|
|
134
|
+
esac
|
|
135
|
+
done
|
|
136
|
+
local result=""
|
|
137
|
+
for seg in "${out[@]}"; do result="$result/$seg"; done
|
|
138
|
+
[ -n "$result" ] || result="/"
|
|
139
|
+
printf '%s' "$result"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Canonicalize an absolute path (resolve .. and, when realpath is available,
|
|
143
|
+
# symlinks). Relative paths are returned unchanged (never match a root).
|
|
144
|
+
canonicalize() {
|
|
145
|
+
local p="$1" out
|
|
146
|
+
case "$p" in
|
|
147
|
+
/*) ;;
|
|
148
|
+
*) printf '%s' "$p"; return 0 ;;
|
|
149
|
+
esac
|
|
150
|
+
if out=$(realpath -m -- "$p" 2>/dev/null) && [ -n "$out" ]; then
|
|
151
|
+
printf '%s' "$out"; return 0
|
|
152
|
+
fi
|
|
153
|
+
lexical_normalize "$p"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
# Root membership (operate on canonical paths only)
|
|
158
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
# True if canonical path lives under ~/.claude/plugins/cache/<owner>/the-bulwark[-*]/...
|
|
161
|
+
# Version-agnostic by design (#15642): the version segment is below the plugin dir.
|
|
162
|
+
is_under_cache_root() {
|
|
163
|
+
local cp="$1" suffix owner rest plugin
|
|
164
|
+
case "$cp" in
|
|
165
|
+
*"/.claude/plugins/cache/"*) ;;
|
|
166
|
+
*) return 1 ;;
|
|
167
|
+
esac
|
|
168
|
+
suffix="${cp#*"/.claude/plugins/cache/"}" # owner/plugin/version/...
|
|
169
|
+
owner="${suffix%%/*}"
|
|
170
|
+
[ -n "$owner" ] && [ "$owner" != "$suffix" ] || return 1
|
|
171
|
+
rest="${suffix#*/}"
|
|
172
|
+
plugin="${rest%%/*}"
|
|
173
|
+
case "$plugin" in
|
|
174
|
+
the-bulwark|the-bulwark-*) return 0 ;;
|
|
175
|
+
esac
|
|
176
|
+
return 1
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# True if canonical path lives under the dev plugin root ($CLAUDE_PLUGIN_ROOT, for --plugin-dir).
|
|
180
|
+
is_under_dev_root() {
|
|
181
|
+
local cp="$1" droot
|
|
182
|
+
[ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || return 1
|
|
183
|
+
droot="$(canonicalize "$CLAUDE_PLUGIN_ROOT")"
|
|
184
|
+
[ -n "$droot" ] || return 1
|
|
185
|
+
case "$cp" in
|
|
186
|
+
"$droot"|"$droot"/*) return 0 ;;
|
|
187
|
+
esac
|
|
188
|
+
return 1
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
is_under_root() {
|
|
192
|
+
is_under_cache_root "$1" || is_under_dev_root "$1"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# True if the RAW (pre-canonical) string references a plugin marker — used to
|
|
196
|
+
# tell a traversal-spoof (deny) apart from an unrelated path (pass-through).
|
|
197
|
+
claims_root() {
|
|
198
|
+
local raw="$1"
|
|
199
|
+
case "$raw" in
|
|
200
|
+
*".claude/plugins/cache/"*"the-bulwark"*) return 0 ;;
|
|
201
|
+
esac
|
|
202
|
+
# Match the literal, unexpanded $CLAUDE_PLUGIN_ROOT token a raw command may
|
|
203
|
+
# carry; single quotes are intentional (SC2016 expected — we match literals).
|
|
204
|
+
# shellcheck disable=SC2016
|
|
205
|
+
case "$raw" in
|
|
206
|
+
*'${CLAUDE_PLUGIN_ROOT}'*|*'$CLAUDE_PLUGIN_ROOT'*) return 0 ;;
|
|
207
|
+
esac
|
|
208
|
+
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
|
209
|
+
case "$raw" in
|
|
210
|
+
*"$CLAUDE_PLUGIN_ROOT"*) return 0 ;;
|
|
211
|
+
esac
|
|
212
|
+
fi
|
|
213
|
+
return 1
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
217
|
+
# Decision logic
|
|
218
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
# Read / Edit: decide on the single file path.
|
|
221
|
+
decide_path() {
|
|
222
|
+
local raw="$1" canon
|
|
223
|
+
canon="$(canonicalize "$(expand_path "$raw")")"
|
|
224
|
+
if [ -n "$canon" ] && is_under_root "$canon"; then
|
|
225
|
+
emit_allow "bulwark-bundled-asset"
|
|
226
|
+
elif claims_root "$raw"; then
|
|
227
|
+
emit_deny "path escapes the Bulwark plugin root" "$raw"
|
|
228
|
+
fi
|
|
229
|
+
exit 0 # pass-through
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Bash: most conservative. Reject anything we cannot reason about simply, then
|
|
233
|
+
# auto-approve ONLY a "run a plugin script" shape: the command verb is itself a
|
|
234
|
+
# plugin-script path (direct exec) OR a bare bash/sh interpreter, AND every
|
|
235
|
+
# path-like token sits under a plugin root. A non-script verb on a plugin path
|
|
236
|
+
# (e.g. `rm <plugin-file>`) is NOT auto-approved — it falls through to CC's
|
|
237
|
+
# default prompt (CR-SUG-1).
|
|
238
|
+
decide_bash() {
|
|
239
|
+
local cmd="$1" expanded tok canon verb
|
|
240
|
+
# Literal shell metacharacters; single quotes are intentional (SC2016 expected).
|
|
241
|
+
# shellcheck disable=SC2016
|
|
242
|
+
case "$cmd" in
|
|
243
|
+
*'&&'*|*'||'*|*';'*|*'|'*|*'$('*|*'`'*|*'>'*|*'<'*|*'&'*|*$'\n'*)
|
|
244
|
+
exit 0 ;; # compound / pipe / subshell / redirect / background -> pass-through
|
|
245
|
+
esac
|
|
246
|
+
expanded="$(expand_path "$cmd")"
|
|
247
|
+
# Word-split WITHOUT globbing (read -ra, default IFS) so a literal '*' in the
|
|
248
|
+
# command is never pathname-expanded during inspection. A path token containing
|
|
249
|
+
# spaces won't match a root cleanly -> falls through to pass-through (SEC-SUG-3).
|
|
250
|
+
local -a toks=()
|
|
251
|
+
read -ra toks <<< "$expanded" || true
|
|
252
|
+
[ "${#toks[@]}" -gt 0 ] || exit 0
|
|
253
|
+
# Command verb must be a plugin script (direct exec) or a bare bash/sh interpreter.
|
|
254
|
+
verb="${toks[0]}"
|
|
255
|
+
local verb_ok=0
|
|
256
|
+
case "$verb" in
|
|
257
|
+
bash|sh) verb_ok=1 ;;
|
|
258
|
+
*/*)
|
|
259
|
+
canon="$(canonicalize "$verb")"
|
|
260
|
+
if [ -n "$canon" ] && is_under_root "$canon"; then verb_ok=1; fi
|
|
261
|
+
;;
|
|
262
|
+
esac
|
|
263
|
+
local has_plugin_token=0 has_escape=0 has_external=0
|
|
264
|
+
for tok in "${toks[@]}"; do
|
|
265
|
+
case "$tok" in
|
|
266
|
+
*/*) ;; # path-like token
|
|
267
|
+
*) continue ;; # flags / bare words -> ignore
|
|
268
|
+
esac
|
|
269
|
+
canon="$(canonicalize "$tok")"
|
|
270
|
+
if [ -n "$canon" ] && is_under_root "$canon"; then
|
|
271
|
+
has_plugin_token=1
|
|
272
|
+
elif claims_root "$tok"; then
|
|
273
|
+
has_escape=1
|
|
274
|
+
else
|
|
275
|
+
has_external=1
|
|
276
|
+
fi
|
|
277
|
+
done
|
|
278
|
+
if [ "$has_escape" -eq 1 ]; then
|
|
279
|
+
emit_deny "command path escapes the Bulwark plugin root" "$cmd"
|
|
280
|
+
elif [ "$verb_ok" -eq 1 ] && [ "$has_plugin_token" -eq 1 ] && [ "$has_external" -eq 0 ]; then
|
|
281
|
+
emit_allow "bulwark-bundled-script"
|
|
282
|
+
fi
|
|
283
|
+
exit 0 # pass-through
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
287
|
+
# Main
|
|
288
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
289
|
+
INPUT="$(cat)"
|
|
290
|
+
TOOL="$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')"
|
|
291
|
+
|
|
292
|
+
case "$TOOL" in
|
|
293
|
+
Read|Edit)
|
|
294
|
+
FP="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')"
|
|
295
|
+
[ -n "$FP" ] || exit 0
|
|
296
|
+
decide_path "$FP"
|
|
297
|
+
;;
|
|
298
|
+
Bash)
|
|
299
|
+
CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""')"
|
|
300
|
+
[ -n "$CMD" ] || exit 0
|
|
301
|
+
decide_bash "$CMD"
|
|
302
|
+
;;
|
|
303
|
+
*)
|
|
304
|
+
exit 0 # not a tool this hook decides on
|
|
305
|
+
;;
|
|
306
|
+
esac
|
|
@@ -3,6 +3,12 @@ name: anthropic-validator
|
|
|
3
3
|
description: Validates Claude Code assets (skills, hooks, agents, commands, MCP servers, plugins) against official Anthropic standards. Fetches latest docs dynamically and produces structured validation reports.
|
|
4
4
|
when_to_use: When validating Claude Code assets (skills, hooks, agents, commands, MCP servers, plugins) against official Anthropic standards before release, after creation, or when auditing for spec compliance.
|
|
5
5
|
user-invocable: true
|
|
6
|
+
allowed-tools:
|
|
7
|
+
- AskUserQuestion
|
|
8
|
+
- Glob
|
|
9
|
+
- Read
|
|
10
|
+
- Task
|
|
11
|
+
- Write
|
|
6
12
|
version: 1.1.1
|
|
7
13
|
author: "Ashay Kubal @ Qball Inc."
|
|
8
14
|
---
|
|
@@ -21,7 +21,8 @@ All skill frontmatter fields are technically optional per the Anthropic spec. `d
|
|
|
21
21
|
| `arguments` | string or YAML list | Argument schema (space-separated string or list) |
|
|
22
22
|
| `disable-model-invocation` | boolean | `true` blocks auto-invocation but keeps `/skill-name` working |
|
|
23
23
|
| `user-invocable` | boolean | `true` to show in `/` menu, `false` to hide |
|
|
24
|
-
| `allowed-tools` | string or YAML list | **
|
|
24
|
+
| `allowed-tools` | string or YAML list | **Pre-authorizes** listed tools (skips approval prompts); does NOT restrict tool availability (NOT `tools`) |
|
|
25
|
+
| `disallowed-tools` | string or YAML list | **Restriction field for SKILLS**: removes tools from the available pool while the skill is active (clears on next message) |
|
|
25
26
|
| `model` | string | `haiku`, `sonnet`, `opus` |
|
|
26
27
|
| `agent` | string | Subagent name to delegate to |
|
|
27
28
|
| `effort` | enum | `low`, `medium`, `high`, `xhigh`, `max` |
|
|
@@ -47,7 +47,8 @@ Per-asset-type workflow + validation points for **skills** (`SKILL.md` files). L
|
|
|
47
47
|
| `arguments` | space-separated string OR YAML list | Optional — argument schema |
|
|
48
48
|
| `disable-model-invocation` | boolean | Optional — `true` blocks auto-invocation but keeps `/` invocation working |
|
|
49
49
|
| `user-invocable` | boolean | Optional — controls `/` menu visibility |
|
|
50
|
-
| `allowed-tools` | space-separated string OR YAML list | Optional — **
|
|
50
|
+
| `allowed-tools` | space-separated string OR YAML list | Optional — **pre-authorizes** the listed tools (skips approval prompts while the skill is active); does NOT restrict which tools are available (NOT `tools`; that field is for AGENTS) |
|
|
51
|
+
| `disallowed-tools` | space-separated/comma string OR YAML list | Optional — **the actual tool-restriction field for SKILLS**: removes the listed tools from Claude's available pool while the skill is active (restriction clears on the next user message) |
|
|
51
52
|
| `model` | string | Optional — `haiku`, `sonnet`, `opus` |
|
|
52
53
|
| `agent` | string (subagent name) | Optional — delegate to a named subagent |
|
|
53
54
|
| `effort` | enum: `low`, `medium`, `high`, `xhigh`, `max` | Optional — model effort level |
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
name: assertion-patterns
|
|
3
3
|
description: Real output verification vs mock calls. Use when transforming T1-T4 violating tests to verify observable behavior.
|
|
4
4
|
user-invocable: false
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
5
8
|
version: 1.0.0
|
|
6
9
|
author: "Ashay Kubal @ Qball Inc."
|
|
7
10
|
---
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
name: bug-magnet-data
|
|
3
3
|
description: Curated edge case test data for boundary testing, verification scripts, and test generation. Provides pre-curated reference data organized by data type with context-specific loading guidance.
|
|
4
4
|
user-invocable: false
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
5
8
|
version: 1.0.0
|
|
6
9
|
author: "Ashay Kubal @ Qball Inc."
|
|
7
10
|
---
|
|
@@ -5,6 +5,14 @@ user-invocable: true
|
|
|
5
5
|
argument-hint: "<topic, filepath, or directory> [--research <synthesis-file>] [--scoped | --exploratory]"
|
|
6
6
|
skills:
|
|
7
7
|
- subagent-prompting
|
|
8
|
+
allowed-tools:
|
|
9
|
+
- AskUserQuestion
|
|
10
|
+
- Bash
|
|
11
|
+
- Glob
|
|
12
|
+
- Read
|
|
13
|
+
- Skill
|
|
14
|
+
- Task
|
|
15
|
+
- Write
|
|
8
16
|
version: 1.0.1
|
|
9
17
|
author: "Ashay Kubal @ Qball Inc."
|
|
10
18
|
---
|