@paths.design/caws-cli 11.1.0 → 11.1.2
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/dist/shell/commands/worktree.d.ts +1 -2
- package/dist/shell/commands/worktree.d.ts.map +1 -1
- package/package.json +6 -3
- package/templates/hook-packs/claude-code/CLAUDE.md +172 -0
- package/templates/hook-packs/claude-code/audit.sh +121 -0
- package/templates/hook-packs/claude-code/block-dangerous.sh +158 -0
- package/templates/hook-packs/claude-code/classify_command.py +1064 -0
- package/templates/hook-packs/claude-code/dispatch/post_tool_use.sh +63 -0
- package/templates/hook-packs/claude-code/dispatch/pre_tool_use.sh +50 -0
- package/templates/hook-packs/claude-code/dispatch/session_start.sh +41 -0
- package/templates/hook-packs/claude-code/dispatch/stop.sh +37 -0
- package/templates/hook-packs/claude-code/guard-strikes.sh +140 -0
- package/templates/hook-packs/claude-code/lib/parse-input.sh +127 -0
- package/templates/hook-packs/claude-code/lib/run-handlers.sh +212 -0
- package/templates/hook-packs/claude-code/reset-danger-latch.sh +21 -0
- package/templates/hook-packs/claude-code/reset-strikes.sh +243 -0
- package/templates/hook-packs/claude-code/runtime-paths.sh +80 -0
- package/templates/hook-packs/claude-code/scope-guard.sh +392 -0
- package/templates/hook-packs/claude-code/session-caws-status.sh +171 -0
- package/templates/hook-packs/claude-code/session-log.sh +180 -0
- package/templates/hook-packs/claude-code/worktree-guard.sh +240 -0
- package/templates/hook-packs/claude-code/worktree-write-guard.sh +77 -0
|
@@ -15,8 +15,7 @@ export interface WorktreeCreateOptions extends BaseCommandOptions {
|
|
|
15
15
|
readonly branch?: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function runWorktreeCreateCommand(opts: WorktreeCreateOptions): number;
|
|
18
|
-
export
|
|
19
|
-
}
|
|
18
|
+
export type WorktreeListOptions = BaseCommandOptions;
|
|
20
19
|
export declare function runWorktreeListCommand(opts?: WorktreeListOptions): number;
|
|
21
20
|
export interface WorktreeBindOptions extends BaseCommandOptions {
|
|
22
21
|
readonly name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../../src/shell/commands/worktree.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,KAAK,SAAS,EAAQ,MAAM,2BAA2B,CAAC;AAcjE,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAkED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,qBAAqB,GAAG,MAAM,CAuC5E;AAID,MAAM,
|
|
1
|
+
{"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../../src/shell/commands/worktree.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,KAAK,SAAS,EAAQ,MAAM,2BAA2B,CAAC;AAcjE,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAkED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,qBAAqB,GAAG,MAAM,CAuC5E;AAID,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AAErD,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,mBAAwB,GAAG,MAAM,CAwB7E;AAID,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CA+BxE;AAID,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CAkC9E;AAID,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CA0C1E"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paths.design/caws-cli",
|
|
3
|
-
"version": "11.1.
|
|
3
|
+
"version": "11.1.2",
|
|
4
4
|
"description": "CAWS CLI - the governed core for CAWS project state, scope, claims, gates, waivers, and evidence (v11.1). Restores canonical spec/worktree lifecycle on the vNext kernel/store/shell architecture.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"preferGlobal": true,
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
"templates/hook-packs/**"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "node scripts/build-cli.js",
|
|
@@ -33,7 +34,9 @@
|
|
|
33
34
|
"validate": "echo 'CLI package validation not required'",
|
|
34
35
|
"caws:validate": "node dist/index.js validate",
|
|
35
36
|
"clean": "rm -rf dist test-caws-project .agent && npm run test:cleanup",
|
|
36
|
-
"prepare": "husky >/dev/null 2>&1 || true"
|
|
37
|
+
"prepare": "husky >/dev/null 2>&1 || true",
|
|
38
|
+
"smoke:fresh-install": "node scripts/fresh-install-smoke.mjs",
|
|
39
|
+
"prepublishOnly": "npm run build && node scripts/fresh-install-smoke.mjs"
|
|
37
40
|
},
|
|
38
41
|
"keywords": [
|
|
39
42
|
"caws",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# CAWS-MANAGED-HOOK
|
|
3
|
+
# hook_pack: claude-code
|
|
4
|
+
# hook_pack_version: 2
|
|
5
|
+
# caws_min_major: 11
|
|
6
|
+
# lineage_refs: 1,4,6,8,11,12,13,16,17
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# CAWS Claude Code Hook Pack
|
|
11
|
+
|
|
12
|
+
This directory contains the v11 Claude Code hook pack — pre-tool-call
|
|
13
|
+
governance infrastructure that interposes between the agent and its Edit,
|
|
14
|
+
Write, and Bash tools. The kernel/store/shell trinity owns canonical state;
|
|
15
|
+
these hooks **project** that state into refusals at the agent's boundary,
|
|
16
|
+
where the kernel cannot reach (the kernel runs downstream of the tool
|
|
17
|
+
call).
|
|
18
|
+
|
|
19
|
+
## The contract: every hook here exists because of an incident
|
|
20
|
+
|
|
21
|
+
This pack is not optional scaffolding. Every script under
|
|
22
|
+
`.claude/hooks/` traces to a specific entry in
|
|
23
|
+
`packages/caws-cli/docs-status/failure-lineage.md`. Modifying or removing
|
|
24
|
+
a hook requires:
|
|
25
|
+
|
|
26
|
+
1. Naming the lineage entry the hook covers.
|
|
27
|
+
2. Identifying the replacement mechanism that preserves the protection.
|
|
28
|
+
3. Documenting the change in the same lineage doc.
|
|
29
|
+
|
|
30
|
+
Hooks may be **evolved** as the v11 state model evolves. They may not be
|
|
31
|
+
removed or weakened by an agent's local judgment. If you think a guard
|
|
32
|
+
is wrong, stop and ask the user.
|
|
33
|
+
|
|
34
|
+
## Lineage map
|
|
35
|
+
|
|
36
|
+
| File | Lineage entries | What it prevents |
|
|
37
|
+
|------|----------------|------------------|
|
|
38
|
+
| `block-dangerous.sh` + `classify_command.py` | 1, 17 | catastrophic git operations; tokenized-argv bypasses; danger latch |
|
|
39
|
+
| `worktree-guard.sh` | 4, 6, 11 | amend/stash/reset/force-push during active worktrees; cross-boundary file copies |
|
|
40
|
+
| `worktree-write-guard.sh` | 4, 8, 13 | base-branch writes when worktrees are active (enforcement returns in CLI-WORKTREE-001); baseline-clobber |
|
|
41
|
+
| `scope-guard.sh` | 8, 11, 12, 16 | edits outside the active spec's `scope.in`; cross-spec union interference; unbound → no authority |
|
|
42
|
+
| `session-caws-status.sh` | 4, 11 | inherited-dirty-state collision; foreign-claim soft-block; version-skew |
|
|
43
|
+
| `reset-strikes.sh` | 8, 16 | human-authorized strike reset (escape hatch, not auto-resettable) |
|
|
44
|
+
| `reset-danger-latch.sh` | 17 | human-authorized danger latch reset |
|
|
45
|
+
| `guard-strikes.sh` | 8, 16 | progressive enforcement (strike 1 warn → strike 3 block) |
|
|
46
|
+
| `audit.sh` | 9 | per-tool-call audit log |
|
|
47
|
+
| `session-log.sh` | 10 | per-turn narrative + structured transcripts |
|
|
48
|
+
| `dispatch/*` | 8, 11, 17 | wires Claude Code's lifecycle to the registered handler list |
|
|
49
|
+
| `lib/*` | 8, 16 | shared input parsing and handler runner |
|
|
50
|
+
|
|
51
|
+
## v11 state-model awareness
|
|
52
|
+
|
|
53
|
+
The v11 pack reads CAWS state under both v10 and v11 shapes during the
|
|
54
|
+
transition window:
|
|
55
|
+
|
|
56
|
+
- **Specs**: `lifecycle_state` is read first; `status` is the v10 fallback.
|
|
57
|
+
Terminal states (closed, archived, completed) are not enforced.
|
|
58
|
+
`draft` does NOT participate in union-wide blocking unless it is the
|
|
59
|
+
authoritative/bound spec.
|
|
60
|
+
- **Worktrees registry**: both v11 direct-key
|
|
61
|
+
(`{"<name>": {...}}`) and v10 nested
|
|
62
|
+
(`{"worktrees": {"<name>": {...}}}`) shapes are accepted.
|
|
63
|
+
- **Bound spec id**: both `entry.specId` (v10) and `entry.spec_id` (v11)
|
|
64
|
+
are accepted.
|
|
65
|
+
|
|
66
|
+
## Version-skew warning
|
|
67
|
+
|
|
68
|
+
`session-caws-status.sh` emits a non-blocking WARNING when the global
|
|
69
|
+
`caws` binary's major version differs from the repo's `caws-cli` major
|
|
70
|
+
version. Hooks parse local state directly, but any CLI advice in
|
|
71
|
+
diagnostics may be invalid. Consider matching major versions:
|
|
72
|
+
`npm install -g @paths.design/caws-cli@^<repo-major>`.
|
|
73
|
+
|
|
74
|
+
## Activation
|
|
75
|
+
|
|
76
|
+
Claude Code reads `.claude/settings.json` at session start. Installing
|
|
77
|
+
the pack mid-session does NOT activate it until the session is restarted.
|
|
78
|
+
`caws init --agent-surface claude-code` prints an activation instruction
|
|
79
|
+
saying so. Do not continue substantive work after install without
|
|
80
|
+
restarting first; the hooks you just installed are not yet enforcing.
|
|
81
|
+
|
|
82
|
+
## settings.json wiring
|
|
83
|
+
|
|
84
|
+
The pack does NOT manage `.claude/settings.json` — that file commonly
|
|
85
|
+
carries user-authored `permissions` and `env` blocks that the pack
|
|
86
|
+
should not overwrite. If you do not have a `.claude/settings.json`, add
|
|
87
|
+
the following minimum configuration so the dispatch entrypoints fire on
|
|
88
|
+
the Claude Code lifecycle:
|
|
89
|
+
|
|
90
|
+
```jsonc
|
|
91
|
+
{
|
|
92
|
+
"hooks": {
|
|
93
|
+
"PreToolUse": [
|
|
94
|
+
{
|
|
95
|
+
"matcher": "Bash|Read|Write|Edit|Glob|Grep|NotebookEdit",
|
|
96
|
+
"hooks": [
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dispatch/pre_tool_use.sh",
|
|
100
|
+
"timeout": 45
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"PostToolUse": [
|
|
106
|
+
{
|
|
107
|
+
"matcher": "Write|Edit|Bash|ExitPlanMode",
|
|
108
|
+
"hooks": [
|
|
109
|
+
{
|
|
110
|
+
"type": "command",
|
|
111
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dispatch/post_tool_use.sh",
|
|
112
|
+
"timeout": 60
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"SessionStart": [
|
|
118
|
+
{
|
|
119
|
+
"hooks": [
|
|
120
|
+
{
|
|
121
|
+
"type": "command",
|
|
122
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dispatch/session_start.sh",
|
|
123
|
+
"timeout": 30
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"Stop": [
|
|
129
|
+
{
|
|
130
|
+
"hooks": [
|
|
131
|
+
{
|
|
132
|
+
"type": "command",
|
|
133
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dispatch/stop.sh",
|
|
134
|
+
"timeout": 30
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"PreCompact": [
|
|
140
|
+
{
|
|
141
|
+
"hooks": [
|
|
142
|
+
{
|
|
143
|
+
"type": "command",
|
|
144
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-log.sh",
|
|
145
|
+
"timeout": 10
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Managed file headers
|
|
155
|
+
|
|
156
|
+
Every managed file in this pack carries a header like:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
# CAWS-MANAGED-HOOK
|
|
160
|
+
# hook_pack: claude-code
|
|
161
|
+
# hook_pack_version: <N>
|
|
162
|
+
# caws_min_major: 11
|
|
163
|
+
# lineage_refs: <comma-separated entries>
|
|
164
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The header is what `caws init` uses to distinguish managed files (safe to
|
|
168
|
+
update on re-install under a documented policy) from local user files
|
|
169
|
+
(refused without explicit `--adopt` or `--overwrite`).
|
|
170
|
+
|
|
171
|
+
Removing or editing the header turns the file into an unmanaged
|
|
172
|
+
snowflake. Re-running install will then refuse to touch it — by design.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS-MANAGED-HOOK
|
|
3
|
+
# hook_pack: claude-code
|
|
4
|
+
# hook_pack_version: 2
|
|
5
|
+
# caws_min_major: 11
|
|
6
|
+
# lineage_refs: 9
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# CAWS Audit Hook for Claude Code
|
|
9
|
+
# Logs agent actions for compliance and debugging
|
|
10
|
+
# @author @darianrosebrook
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
# shellcheck source=lib/parse-input.sh
|
|
16
|
+
source "$SCRIPT_DIR/lib/parse-input.sh"
|
|
17
|
+
|
|
18
|
+
# --- CWD resilience ---
|
|
19
|
+
# PostToolUse hooks fire AFTER the command runs. If the command destroyed
|
|
20
|
+
# the working directory (e.g., caws worktree merge deletes the worktree),
|
|
21
|
+
# the hook process inherits a nonexistent CWD and most commands will fail.
|
|
22
|
+
# Recover to a safe directory before doing anything else.
|
|
23
|
+
if ! pwd >/dev/null 2>&1 || [ ! -d "$(pwd 2>/dev/null || echo __gone__)" ]; then
|
|
24
|
+
cd "${CLAUDE_PROJECT_DIR:-$HOME}" 2>/dev/null || cd "$HOME"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
parse_hook_input
|
|
28
|
+
|
|
29
|
+
# Get event type from argument or input
|
|
30
|
+
EVENT_TYPE="${1:-tool-use}"
|
|
31
|
+
|
|
32
|
+
# Back-compat aliases. HOOK_CWD can be "" when stdin lacks a cwd field;
|
|
33
|
+
# audit.sh always wants a non-empty placeholder so jq --arg stays happy.
|
|
34
|
+
SESSION_ID="$HOOK_SESSION_ID"
|
|
35
|
+
CWD="${HOOK_CWD:-.}"
|
|
36
|
+
HOOK_EVENT="${HOOK_EVENT_NAME:-unknown}"
|
|
37
|
+
TOOL_NAME="$HOOK_TOOL_NAME"
|
|
38
|
+
PERMISSION_MODE="$HOOK_PERMISSION_MODE"
|
|
39
|
+
|
|
40
|
+
# Ensure log directory exists
|
|
41
|
+
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/logs"
|
|
42
|
+
mkdir -p "$LOG_DIR"
|
|
43
|
+
|
|
44
|
+
# Log file path
|
|
45
|
+
LOG_FILE="$LOG_DIR/audit.log"
|
|
46
|
+
DATE_LOG_FILE="$LOG_DIR/audit-$(date +%Y-%m-%d).log"
|
|
47
|
+
|
|
48
|
+
# Timestamp
|
|
49
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
50
|
+
|
|
51
|
+
# Build log entry based on event type
|
|
52
|
+
case "$EVENT_TYPE" in
|
|
53
|
+
session-start)
|
|
54
|
+
SOURCE="${HOOK_SOURCE:-unknown}"
|
|
55
|
+
MODEL="${HOOK_MODEL:-unknown}"
|
|
56
|
+
LOG_ENTRY=$(jq -n \
|
|
57
|
+
--arg ts "$TIMESTAMP" \
|
|
58
|
+
--arg sid "$SESSION_ID" \
|
|
59
|
+
--arg event "session_start" \
|
|
60
|
+
--arg source "$SOURCE" \
|
|
61
|
+
--arg model "$MODEL" \
|
|
62
|
+
--arg cwd "$CWD" \
|
|
63
|
+
'{timestamp: $ts, session_id: $sid, event: $event, source: $source, model: $model, cwd: $cwd}')
|
|
64
|
+
;;
|
|
65
|
+
|
|
66
|
+
stop)
|
|
67
|
+
# HOOK_STOP_HOOK_ACTIVE is "0" or "1"; jq --argjson needs true/false.
|
|
68
|
+
if [[ "$HOOK_STOP_HOOK_ACTIVE" == "1" ]]; then
|
|
69
|
+
STOP_HOOK_ACTIVE="true"
|
|
70
|
+
else
|
|
71
|
+
STOP_HOOK_ACTIVE="false"
|
|
72
|
+
fi
|
|
73
|
+
LOG_ENTRY=$(jq -n \
|
|
74
|
+
--arg ts "$TIMESTAMP" \
|
|
75
|
+
--arg sid "$SESSION_ID" \
|
|
76
|
+
--arg event "session_stop" \
|
|
77
|
+
--arg cwd "$CWD" \
|
|
78
|
+
--argjson hook_active "$STOP_HOOK_ACTIVE" \
|
|
79
|
+
'{timestamp: $ts, session_id: $sid, event: $event, cwd: $cwd, stop_hook_active: $hook_active}')
|
|
80
|
+
;;
|
|
81
|
+
|
|
82
|
+
tool-use)
|
|
83
|
+
# Tool-specific info lifted from HOOK_* env vars set by parse_hook_input.
|
|
84
|
+
# HOOK_TOOL_INPUT_JSON and HOOK_TOOL_RESPONSE_JSON are pre-serialized
|
|
85
|
+
# JSON strings, always valid (empty "{}" at minimum), so jq --argjson
|
|
86
|
+
# below never trips on missing fields.
|
|
87
|
+
TOOL_INPUT="$HOOK_TOOL_INPUT_JSON"
|
|
88
|
+
TOOL_RESPONSE="$HOOK_TOOL_RESPONSE_JSON"
|
|
89
|
+
TOOL_USE_ID="$HOOK_TOOL_USE_ID"
|
|
90
|
+
FILE_PATH="$HOOK_FILE_PATH"
|
|
91
|
+
COMMAND="$HOOK_COMMAND"
|
|
92
|
+
|
|
93
|
+
LOG_ENTRY=$(jq -n \
|
|
94
|
+
--arg ts "$TIMESTAMP" \
|
|
95
|
+
--arg sid "$SESSION_ID" \
|
|
96
|
+
--arg event "tool_use" \
|
|
97
|
+
--arg tool "$TOOL_NAME" \
|
|
98
|
+
--arg file "$FILE_PATH" \
|
|
99
|
+
--arg cmd "$COMMAND" \
|
|
100
|
+
--arg cwd "$CWD" \
|
|
101
|
+
--arg mode "$PERMISSION_MODE" \
|
|
102
|
+
'{timestamp: $ts, session_id: $sid, event: $event, tool: $tool, file: $file, command: $cmd, cwd: $cwd, permission_mode: $mode}')
|
|
103
|
+
;;
|
|
104
|
+
|
|
105
|
+
*)
|
|
106
|
+
LOG_ENTRY=$(jq -n \
|
|
107
|
+
--arg ts "$TIMESTAMP" \
|
|
108
|
+
--arg sid "$SESSION_ID" \
|
|
109
|
+
--arg event "$EVENT_TYPE" \
|
|
110
|
+
--arg hook "$HOOK_EVENT" \
|
|
111
|
+
--arg cwd "$CWD" \
|
|
112
|
+
'{timestamp: $ts, session_id: $sid, event: $event, hook_event: $hook, cwd: $cwd}')
|
|
113
|
+
;;
|
|
114
|
+
esac
|
|
115
|
+
|
|
116
|
+
# Append to log files
|
|
117
|
+
echo "$LOG_ENTRY" >> "$LOG_FILE"
|
|
118
|
+
echo "$LOG_ENTRY" >> "$DATE_LOG_FILE"
|
|
119
|
+
|
|
120
|
+
# Success - allow operation to continue
|
|
121
|
+
exit 0
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS-MANAGED-HOOK
|
|
3
|
+
# hook_pack: claude-code
|
|
4
|
+
# hook_pack_version: 2
|
|
5
|
+
# caws_min_major: 11
|
|
6
|
+
# lineage_refs: 1,17
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# CAWS Command Safety Gate for Claude Code
|
|
9
|
+
# Delegates to classify_command.py for robust command parsing and classification.
|
|
10
|
+
# Falls back to bash pattern matching if Python is unavailable.
|
|
11
|
+
#
|
|
12
|
+
# The Python classifier handles:
|
|
13
|
+
# - Heredoc-aware parsing (won't false-positive on quoted dangerous commands)
|
|
14
|
+
# - Quoted-region stripping (echo "git reset --hard" is safe)
|
|
15
|
+
# - Pipeline-aware dangers (curl | sh)
|
|
16
|
+
# - Context-aware rm classification (safe prefixes vs dangerous targets)
|
|
17
|
+
# - Proper shell segmentation (&&, ||, ;, |)
|
|
18
|
+
#
|
|
19
|
+
# @author @darianrosebrook
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
|
|
25
|
+
danger_state_dir() {
|
|
26
|
+
local project_dir="${CLAUDE_PROJECT_DIR:-.}"
|
|
27
|
+
local state_dir="$project_dir/.claude/hooks/state"
|
|
28
|
+
mkdir -p "$state_dir"
|
|
29
|
+
printf '%s\n' "$state_dir"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
danger_latch_file() {
|
|
33
|
+
local session_id="$1"
|
|
34
|
+
local safe_session
|
|
35
|
+
safe_session=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
|
36
|
+
printf '%s/danger-latch-%s.json\n' "$(danger_state_dir)" "$safe_session"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
emit_block_json() {
|
|
40
|
+
local reason="$1"
|
|
41
|
+
jq -n --arg msg "$reason" '{ decision: "block", reason: $msg }'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
emit_ask_json() {
|
|
45
|
+
local reason="$1"
|
|
46
|
+
jq -n --arg msg "$reason" '{
|
|
47
|
+
hookSpecificOutput: {
|
|
48
|
+
hookEventName: "PreToolUse",
|
|
49
|
+
permissionDecision: "ask",
|
|
50
|
+
permissionDecisionReason: $msg
|
|
51
|
+
}
|
|
52
|
+
}'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
record_danger_latch() {
|
|
56
|
+
local file="$1"
|
|
57
|
+
local decision="$2"
|
|
58
|
+
local reason="$3"
|
|
59
|
+
local command="$4"
|
|
60
|
+
|
|
61
|
+
mkdir -p "$(dirname "$file")"
|
|
62
|
+
jq -n \
|
|
63
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
64
|
+
--arg hook "block-dangerous.sh" \
|
|
65
|
+
--arg decision "$decision" \
|
|
66
|
+
--arg reason "$reason" \
|
|
67
|
+
--arg command "$command" \
|
|
68
|
+
'{
|
|
69
|
+
ts: $ts,
|
|
70
|
+
hook: $hook,
|
|
71
|
+
decision: $decision,
|
|
72
|
+
reason: $reason,
|
|
73
|
+
command: $command,
|
|
74
|
+
message: "Dangerous command boundary engaged. User reset required before more Bash commands may run in this session."
|
|
75
|
+
}' > "$file"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Read JSON input from Claude Code
|
|
79
|
+
INPUT=$(cat)
|
|
80
|
+
|
|
81
|
+
# Extract tool info
|
|
82
|
+
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')
|
|
83
|
+
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""')
|
|
84
|
+
# Fallback to "unknown" when no session id is available so the latch still
|
|
85
|
+
# engages. Multiple concurrent sessions without an id will share the "unknown"
|
|
86
|
+
# latch -- safer than not latching at all.
|
|
87
|
+
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // env.CLAUDE_SESSION_ID // env.HOOK_SESSION_ID // "unknown"')
|
|
88
|
+
|
|
89
|
+
# Only check Bash tool
|
|
90
|
+
if [[ "$TOOL_NAME" != "Bash" ]] || [[ -z "$COMMAND" ]]; then
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
LATCH_FILE="$(danger_latch_file "$SESSION_ID")"
|
|
95
|
+
if [[ -f "$LATCH_FILE" ]]; then
|
|
96
|
+
REASON="A dangerous command was previously blocked or sent for approval in this Claude session. This is a human-review boundary, not a retryable syntax error. Do not rephrase, wrap, reorder, alias, or indirectly invoke the command. Ask the user to clear the latch with .claude/hooks/reset-danger-latch.sh before more Bash commands may run. Sentinel: $LATCH_FILE"
|
|
97
|
+
emit_block_json "$REASON"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# --- Python classifier (preferred path) ---
|
|
102
|
+
CLASSIFIER="$SCRIPT_DIR/classify_command.py"
|
|
103
|
+
if [[ ! -f "$CLASSIFIER" ]] || ! command -v python3 >/dev/null 2>&1; then
|
|
104
|
+
REASON="command classifier unavailable; dangerous-command safety cannot verify Bash semantics. This is a human-review boundary. Command was: $COMMAND"
|
|
105
|
+
record_danger_latch "$LATCH_FILE" "ask" "classifier unavailable" "$COMMAND"
|
|
106
|
+
emit_ask_json "$REASON"
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
REPO_ROOT="${CLAUDE_PROJECT_DIR:-.}"
|
|
111
|
+
CLASSIFIER_STDERR=$(mktemp)
|
|
112
|
+
RESULT=$(printf '%s' "$COMMAND" | python3 "$CLASSIFIER" \
|
|
113
|
+
--repo-root "$REPO_ROOT" \
|
|
114
|
+
--home "$HOME" \
|
|
115
|
+
--cwd "$(pwd)" 2>"$CLASSIFIER_STDERR") || {
|
|
116
|
+
DIAG=$(head -c 200 "$CLASSIFIER_STDERR" 2>/dev/null || true)
|
|
117
|
+
rm -f "$CLASSIFIER_STDERR"
|
|
118
|
+
RESULT="{\"decision\":\"ask\",\"reason\":\"command classifier failed: ${DIAG:-unknown error}\"}"
|
|
119
|
+
}
|
|
120
|
+
rm -f "$CLASSIFIER_STDERR"
|
|
121
|
+
|
|
122
|
+
DECISION=$(printf '%s' "$RESULT" | jq -r '.decision // "ask"')
|
|
123
|
+
REASON=$(printf '%s' "$RESULT" | jq -r '.reason // "unknown"')
|
|
124
|
+
|
|
125
|
+
case "$DECISION" in
|
|
126
|
+
allow)
|
|
127
|
+
exit 0
|
|
128
|
+
;;
|
|
129
|
+
deny)
|
|
130
|
+
FULL_REASON="$REASON. This is a HARD BLOCK — Claude Code will refuse the command. This is a human-review boundary, not a retryable syntax error. Do not rephrase, wrap, reorder, alias, or indirectly invoke this command (e.g. via 'command git ...', 'env ... git ...', 'bash -lc \"...\"', or 'git --bare init'). Stop and ask the user for the next step. Command was: $COMMAND"
|
|
131
|
+
record_danger_latch "$LATCH_FILE" "$DECISION" "$REASON" "$COMMAND"
|
|
132
|
+
emit_block_json "$FULL_REASON"
|
|
133
|
+
exit 0
|
|
134
|
+
;;
|
|
135
|
+
ask)
|
|
136
|
+
FULL_REASON="$REASON. Claude Code will PAUSE and ask the user to approve before running. This may alter destructive or authority-bearing state. Do not attempt to bypass this by rephrasing the command, switching syntax, or wrapping the invocation. If permission is not granted, stop and ask the user for the next step. Command was: $COMMAND"
|
|
137
|
+
record_danger_latch "$LATCH_FILE" "$DECISION" "$REASON" "$COMMAND"
|
|
138
|
+
emit_ask_json "$FULL_REASON"
|
|
139
|
+
exit 0
|
|
140
|
+
;;
|
|
141
|
+
*)
|
|
142
|
+
# Unknown decision value -- malformed classifier output. Do NOT fall
|
|
143
|
+
# through to the weaker regex fallback; ask+latch instead so a
|
|
144
|
+
# corrupted classifier cannot silently downgrade safety.
|
|
145
|
+
FULL_REASON="command classifier returned an unrecognized decision '$DECISION'. Claude Code will PAUSE and ask the user. This is a human-review boundary. Command was: $COMMAND"
|
|
146
|
+
record_danger_latch "$LATCH_FILE" "ask" "classifier unknown decision: $DECISION" "$COMMAND"
|
|
147
|
+
emit_ask_json "$FULL_REASON"
|
|
148
|
+
exit 0
|
|
149
|
+
;;
|
|
150
|
+
esac
|
|
151
|
+
|
|
152
|
+
# Every classifier outcome (allow/deny/ask/unknown) exits inside the case
|
|
153
|
+
# above. There is no flat-regex fallback; if classify_command.py cannot run,
|
|
154
|
+
# the early-exit at the top of this script ask-latches the command. That
|
|
155
|
+
# keeps the dangerous-command decision in a single semantic layer.
|
|
156
|
+
|
|
157
|
+
# shellcheck disable=SC2317 # Defense-in-depth tail; unreachable on a healthy classifier.
|
|
158
|
+
exit 0
|