@paths.design/caws-cli 11.0.0 → 11.1.1

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.
Files changed (119) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +2 -2
  3. package/dist/init/harness-detect.d.ts +18 -0
  4. package/dist/init/harness-detect.d.ts.map +1 -0
  5. package/dist/init/harness-detect.js +90 -0
  6. package/dist/init/harness-detect.js.map +1 -0
  7. package/dist/init/hook-install.d.ts +53 -0
  8. package/dist/init/hook-install.d.ts.map +1 -0
  9. package/dist/init/hook-install.js +421 -0
  10. package/dist/init/hook-install.js.map +1 -0
  11. package/dist/init/hook-packs/manifest-claude-code.d.ts +4 -0
  12. package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -0
  13. package/dist/init/hook-packs/manifest-claude-code.js +190 -0
  14. package/dist/init/hook-packs/manifest-claude-code.js.map +1 -0
  15. package/dist/init/hook-packs/register.d.ts +19 -0
  16. package/dist/init/hook-packs/register.d.ts.map +1 -0
  17. package/dist/init/hook-packs/register.js +37 -0
  18. package/dist/init/hook-packs/register.js.map +1 -0
  19. package/dist/init/hook-packs/types.d.ts +123 -0
  20. package/dist/init/hook-packs/types.d.ts.map +1 -0
  21. package/dist/init/hook-packs/types.js +29 -0
  22. package/dist/init/hook-packs/types.js.map +1 -0
  23. package/dist/shell/commands/gates.d.ts.map +1 -1
  24. package/dist/shell/commands/gates.js +28 -1
  25. package/dist/shell/commands/gates.js.map +1 -1
  26. package/dist/shell/commands/init.d.ts +9 -0
  27. package/dist/shell/commands/init.d.ts.map +1 -1
  28. package/dist/shell/commands/init.js +131 -27
  29. package/dist/shell/commands/init.js.map +1 -1
  30. package/dist/shell/commands/specs.d.ts +41 -0
  31. package/dist/shell/commands/specs.d.ts.map +1 -0
  32. package/dist/shell/commands/specs.js +264 -0
  33. package/dist/shell/commands/specs.js.map +1 -0
  34. package/dist/shell/commands/worktree.d.ts +38 -0
  35. package/dist/shell/commands/worktree.d.ts.map +1 -0
  36. package/dist/shell/commands/worktree.js +286 -0
  37. package/dist/shell/commands/worktree.js.map +1 -0
  38. package/dist/shell/gates/disposition.d.ts.map +1 -1
  39. package/dist/shell/gates/disposition.js +33 -3
  40. package/dist/shell/gates/disposition.js.map +1 -1
  41. package/dist/shell/gates/local-evaluators/budget-limit.d.ts +24 -0
  42. package/dist/shell/gates/local-evaluators/budget-limit.d.ts.map +1 -0
  43. package/dist/shell/gates/local-evaluators/budget-limit.js +67 -0
  44. package/dist/shell/gates/local-evaluators/budget-limit.js.map +1 -0
  45. package/dist/shell/gates/local-evaluators/diff-helpers.d.ts +25 -0
  46. package/dist/shell/gates/local-evaluators/diff-helpers.d.ts.map +1 -0
  47. package/dist/shell/gates/local-evaluators/diff-helpers.js +74 -0
  48. package/dist/shell/gates/local-evaluators/diff-helpers.js.map +1 -0
  49. package/dist/shell/gates/local-evaluators/index.d.ts +28 -0
  50. package/dist/shell/gates/local-evaluators/index.d.ts.map +1 -0
  51. package/dist/shell/gates/local-evaluators/index.js +67 -0
  52. package/dist/shell/gates/local-evaluators/index.js.map +1 -0
  53. package/dist/shell/gates/local-evaluators/scope-boundary.d.ts +23 -0
  54. package/dist/shell/gates/local-evaluators/scope-boundary.d.ts.map +1 -0
  55. package/dist/shell/gates/local-evaluators/scope-boundary.js +67 -0
  56. package/dist/shell/gates/local-evaluators/scope-boundary.js.map +1 -0
  57. package/dist/shell/gates/local-evaluators/spec-completeness.d.ts +12 -0
  58. package/dist/shell/gates/local-evaluators/spec-completeness.d.ts.map +1 -0
  59. package/dist/shell/gates/local-evaluators/spec-completeness.js +73 -0
  60. package/dist/shell/gates/local-evaluators/spec-completeness.js.map +1 -0
  61. package/dist/shell/index.d.ts +4 -0
  62. package/dist/shell/index.d.ts.map +1 -1
  63. package/dist/shell/index.js +13 -1
  64. package/dist/shell/index.js.map +1 -1
  65. package/dist/shell/register.d.ts.map +1 -1
  66. package/dist/shell/register.js +192 -2
  67. package/dist/shell/register.js.map +1 -1
  68. package/dist/shell/render/init-hook-pack.d.ts +16 -0
  69. package/dist/shell/render/init-hook-pack.d.ts.map +1 -0
  70. package/dist/shell/render/init-hook-pack.js +206 -0
  71. package/dist/shell/render/init-hook-pack.js.map +1 -0
  72. package/dist/store/atomic-write.d.ts +20 -2
  73. package/dist/store/atomic-write.d.ts.map +1 -1
  74. package/dist/store/atomic-write.js +44 -2
  75. package/dist/store/atomic-write.js.map +1 -1
  76. package/dist/store/lifecycle-lock.d.ts +34 -0
  77. package/dist/store/lifecycle-lock.d.ts.map +1 -0
  78. package/dist/store/lifecycle-lock.js +168 -0
  79. package/dist/store/lifecycle-lock.js.map +1 -0
  80. package/dist/store/lifecycle-transaction.d.ts +79 -0
  81. package/dist/store/lifecycle-transaction.d.ts.map +1 -0
  82. package/dist/store/lifecycle-transaction.js +319 -0
  83. package/dist/store/lifecycle-transaction.js.map +1 -0
  84. package/dist/store/rules.d.ts +16 -0
  85. package/dist/store/rules.d.ts.map +1 -1
  86. package/dist/store/rules.js +17 -0
  87. package/dist/store/rules.js.map +1 -1
  88. package/dist/store/specs-writer.d.ts +61 -0
  89. package/dist/store/specs-writer.d.ts.map +1 -0
  90. package/dist/store/specs-writer.js +506 -0
  91. package/dist/store/specs-writer.js.map +1 -0
  92. package/dist/store/worktrees-writer.d.ts +77 -0
  93. package/dist/store/worktrees-writer.d.ts.map +1 -0
  94. package/dist/store/worktrees-writer.js +674 -0
  95. package/dist/store/worktrees-writer.js.map +1 -0
  96. package/dist/store/yaml-patch.d.ts +7 -0
  97. package/dist/store/yaml-patch.d.ts.map +1 -0
  98. package/dist/store/yaml-patch.js +250 -0
  99. package/dist/store/yaml-patch.js.map +1 -0
  100. package/package.json +7 -4
  101. package/templates/hook-packs/claude-code/CLAUDE.md +172 -0
  102. package/templates/hook-packs/claude-code/audit.sh +121 -0
  103. package/templates/hook-packs/claude-code/block-dangerous.sh +158 -0
  104. package/templates/hook-packs/claude-code/classify_command.py +1064 -0
  105. package/templates/hook-packs/claude-code/dispatch/post_tool_use.sh +63 -0
  106. package/templates/hook-packs/claude-code/dispatch/pre_tool_use.sh +50 -0
  107. package/templates/hook-packs/claude-code/dispatch/session_start.sh +41 -0
  108. package/templates/hook-packs/claude-code/dispatch/stop.sh +37 -0
  109. package/templates/hook-packs/claude-code/guard-strikes.sh +140 -0
  110. package/templates/hook-packs/claude-code/lib/parse-input.sh +127 -0
  111. package/templates/hook-packs/claude-code/lib/run-handlers.sh +212 -0
  112. package/templates/hook-packs/claude-code/reset-danger-latch.sh +21 -0
  113. package/templates/hook-packs/claude-code/reset-strikes.sh +243 -0
  114. package/templates/hook-packs/claude-code/runtime-paths.sh +80 -0
  115. package/templates/hook-packs/claude-code/scope-guard.sh +392 -0
  116. package/templates/hook-packs/claude-code/session-caws-status.sh +171 -0
  117. package/templates/hook-packs/claude-code/session-log.sh +180 -0
  118. package/templates/hook-packs/claude-code/worktree-guard.sh +240 -0
  119. package/templates/hook-packs/claude-code/worktree-write-guard.sh +77 -0
@@ -0,0 +1,180 @@
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: 10
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ # Session Logger for Claude Code — lean structured session capture.
9
+ #
10
+ # Canonical artifacts:
11
+ # session.json — session index + aggregated refs + git snapshot
12
+ # turn-001.json — per-turn detailed timeline
13
+ # handoff.json — compact continuation view for follow-on agents
14
+ # session.txt — human-readable summary pointing at the JSON artifacts
15
+ #
16
+ # Output: ./tmp/<session-id>/
17
+
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ # shellcheck source=lib/parse-input.sh
22
+ source "$SCRIPT_DIR/lib/parse-input.sh"
23
+ parse_hook_input
24
+
25
+ SESSION_ID="$HOOK_SESSION_ID"
26
+ HOOK_EVENT="${HOOK_EVENT_NAME:-unknown}"
27
+ CWD="${HOOK_CWD:-.}"
28
+ TRANSCRIPT_PATH="$HOOK_TRANSCRIPT_PATH"
29
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
30
+
31
+ LOG_DIR="${CWD}/tmp/${SESSION_ID}"
32
+ mkdir -p "$LOG_DIR"
33
+
34
+ META_FILE="$LOG_DIR/.meta.json"
35
+ RENDERER="$SCRIPT_DIR/session_log_renderer.py"
36
+
37
+ resolve_transcript() {
38
+ if [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
39
+ printf '%s\n' "$TRANSCRIPT_PATH"
40
+ return
41
+ fi
42
+
43
+ local slug candidate
44
+ slug=$(echo "$CWD" | sed 's|/|-|g; s|^-||')
45
+
46
+ candidate="$HOME/.claude/projects/${slug}/${SESSION_ID}.jsonl"
47
+ if [[ -f "$candidate" ]]; then
48
+ printf '%s\n' "$candidate"
49
+ return
50
+ fi
51
+
52
+ candidate="$HOME/.claude/projects/-${slug}/${SESSION_ID}.jsonl"
53
+ if [[ -f "$candidate" ]]; then
54
+ printf '%s\n' "$candidate"
55
+ return
56
+ fi
57
+
58
+ printf '\n'
59
+ }
60
+
61
+ render_session_output() {
62
+ local transcript="$1"
63
+ local branch head_sha dirty_count started_at model start_sha
64
+
65
+ if cd "$CWD" 2>/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
66
+ branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
67
+ head_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
68
+ dirty_count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
69
+ else
70
+ branch="unknown"
71
+ head_sha="unknown"
72
+ dirty_count="0"
73
+ fi
74
+
75
+ if [[ -f "$META_FILE" ]]; then
76
+ started_at=$(jq -r '.local_time // "unknown"' "$META_FILE")
77
+ model=$(jq -r '.model // "unknown"' "$META_FILE")
78
+ start_sha=$(jq -r '.head_sha // ""' "$META_FILE")
79
+ else
80
+ started_at="(resumed session)"
81
+ model="unknown"
82
+ start_sha=""
83
+ fi
84
+
85
+ python3 "$RENDERER" \
86
+ "$LOG_DIR" \
87
+ "$CWD" \
88
+ "$SESSION_ID" \
89
+ "$started_at" \
90
+ "$model" \
91
+ "$branch" \
92
+ "$head_sha" \
93
+ "$dirty_count" \
94
+ "$start_sha" \
95
+ "$transcript"
96
+ }
97
+
98
+ handle_session_start() {
99
+ local model source branch head_sha dirty_count full_time
100
+ model="${HOOK_MODEL:-unknown}"
101
+ source="${HOOK_SOURCE:-unknown}"
102
+ if cd "$CWD" 2>/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
103
+ branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
104
+ head_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
105
+ dirty_count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
106
+ else
107
+ branch="unknown"
108
+ head_sha="unknown"
109
+ dirty_count="0"
110
+ fi
111
+ full_time=$(date +"%Y-%m-%d %H:%M:%S %Z")
112
+
113
+ jq -cn \
114
+ --arg sid "$SESSION_ID" \
115
+ --arg ts "$TIMESTAMP" \
116
+ --arg lt "$full_time" \
117
+ --arg model "$model" \
118
+ --arg source "$source" \
119
+ --arg branch "$branch" \
120
+ --arg head "$head_sha" \
121
+ --arg dirty "$dirty_count" \
122
+ --arg project "$(basename "$CWD")" \
123
+ --arg transcript "$TRANSCRIPT_PATH" \
124
+ '{session_id: $sid, started_at: $ts, local_time: $lt, model: $model, source: $source, branch: $branch, head_sha: $head, dirty_files: $dirty, project: $project, transcript_path: $transcript}' \
125
+ > "$META_FILE"
126
+
127
+ render_session_output "$(resolve_transcript)"
128
+ }
129
+
130
+ handle_stop() {
131
+ render_session_output "$(resolve_transcript)"
132
+ }
133
+
134
+ handle_pre_compact() {
135
+ render_session_output "$(resolve_transcript)"
136
+ }
137
+
138
+ is_plan_file_path() {
139
+ local file_path
140
+ file_path="${1:-}"
141
+
142
+ [[ -n "$file_path" ]] || return 1
143
+
144
+ case "$file_path" in
145
+ "$HOME"/.claude/plans/*.md|*/.claude/plans/*.md)
146
+ return 0
147
+ ;;
148
+ *)
149
+ return 1
150
+ ;;
151
+ esac
152
+ }
153
+
154
+ handle_post_tool_use() {
155
+ local tool_name file_path
156
+ tool_name="$HOOK_TOOL_NAME"
157
+ file_path="${HOOK_FILE_PATH:-}"
158
+ case "$tool_name" in
159
+ Write|Edit)
160
+ if is_plan_file_path "$file_path"; then
161
+ render_session_output "$(resolve_transcript)"
162
+ fi
163
+ ;;
164
+ ExitPlanMode)
165
+ render_session_output "$(resolve_transcript)"
166
+ ;;
167
+ *)
168
+ ;;
169
+ esac
170
+ }
171
+
172
+ case "$HOOK_EVENT" in
173
+ SessionStart) handle_session_start ;;
174
+ Stop) handle_stop ;;
175
+ PreCompact) handle_pre_compact ;;
176
+ PostToolUse) handle_post_tool_use ;;
177
+ *) ;;
178
+ esac
179
+
180
+ exit 0
@@ -0,0 +1,240 @@
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: 4,6,11
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ #
9
+ # CAWS Worktree Safety Guard for Claude Code (v11-shape).
10
+ # Blocks dangerous git operations and cross-boundary file copies when
11
+ # parallel worktrees are active.
12
+ #
13
+ # Registry-shape compatibility:
14
+ # v11 direct-key: { "<name>": { "status": "active", ... } }
15
+ # v10 nested: { "worktrees": { "<name>": { ... } } }
16
+
17
+ set -euo pipefail
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ # shellcheck source=lib/parse-input.sh
21
+ source "$SCRIPT_DIR/lib/parse-input.sh"
22
+ parse_hook_input
23
+
24
+ TOOL_NAME="$HOOK_TOOL_NAME"
25
+ COMMAND="$HOOK_COMMAND"
26
+
27
+ if [[ "$TOOL_NAME" != "Bash" ]] || [[ -z "$COMMAND" ]]; then
28
+ exit 0
29
+ fi
30
+
31
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
32
+
33
+ if command -v git >/dev/null 2>&1; then
34
+ GIT_COMMON_DIR=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
35
+ if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != ".git" ]]; then
36
+ CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON_DIR/.." 2>/dev/null && pwd || echo "")
37
+ if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
38
+ PROJECT_DIR="$CANDIDATE"
39
+ fi
40
+ fi
41
+ fi
42
+
43
+ # Block sparse checkout (runs before "only check git commands" early-exit)
44
+ if echo "$COMMAND" | grep -qE 'caws\s+(worktree\s+create|parallel\s+setup).*--scope'; then
45
+ echo "BLOCKED: --scope (sparse checkout) is not allowed." >&2
46
+ echo "Sparse checkout breaks cross-module imports in most projects." >&2
47
+ echo "Use full worktrees without --scope. Scope enforcement comes from" >&2
48
+ echo "CAWS feature specs and lane discipline, not from hiding files." >&2
49
+ exit 2
50
+ fi
51
+
52
+ if echo "$COMMAND" | grep -qE '(^|;|&&|\|)\s*git\s+sparse-checkout'; then
53
+ echo "BLOCKED: git sparse-checkout is not allowed in this project." >&2
54
+ echo "Use full worktrees without sparse-checkout." >&2
55
+ exit 2
56
+ fi
57
+
58
+ # Block cross-boundary file copies (worktree → main).
59
+ WORKTREE_BASE="$PROJECT_DIR/.caws/worktrees"
60
+ if [[ -d "$WORKTREE_BASE" ]]; then
61
+ if echo "$COMMAND" | grep -qE '\b(cp|mv)\b'; then
62
+ AGENT_IN_WORKTREE=false
63
+ if [[ -n "$HOOK_CWD" ]] && [[ "$HOOK_CWD" == "$WORKTREE_BASE"/* ]]; then
64
+ AGENT_IN_WORKTREE=true
65
+ fi
66
+
67
+ if [[ "$AGENT_IN_WORKTREE" != "true" ]]; then
68
+ if echo "$COMMAND" | grep -qF ".caws/worktrees/" || echo "$COMMAND" | grep -qF "$WORKTREE_BASE"; then
69
+ HAS_WT_PATH=false
70
+ HAS_MAIN_PATH=false
71
+ if echo "$COMMAND" | grep -qE '\.caws/worktrees/|'"$(echo "$WORKTREE_BASE" | sed 's/[\/&]/\\&/g')"''; then
72
+ HAS_WT_PATH=true
73
+ fi
74
+ if echo "$COMMAND" | grep -qE "(^|\s)$PROJECT_DIR/[^.]|core/|src/|tests/|packages/" && [[ "$HAS_WT_PATH" == "true" ]]; then
75
+ HAS_MAIN_PATH=true
76
+ fi
77
+ if [[ "$HAS_WT_PATH" == "true" ]] && [[ "$HAS_MAIN_PATH" == "true" ]]; then
78
+ echo "BLOCKED: Copying files from a worktree to the main repo is forbidden." >&2
79
+ echo "This bypasses worktree isolation. Work entirely within your worktree." >&2
80
+ echo "If tests need the main repo's venv, activate it with:" >&2
81
+ echo " source $PROJECT_DIR/.venv/bin/activate" >&2
82
+ exit 2
83
+ fi
84
+ fi
85
+ fi
86
+ fi
87
+ fi
88
+
89
+ # Only check git commands from here on
90
+ if ! echo "$COMMAND" | grep -qE '(^|\s|&&|\|)git\s'; then
91
+ exit 0
92
+ fi
93
+
94
+ # Determine if worktrees are active (dual-shape aware).
95
+ #
96
+ # Helper logic embedded in the Node inline reads worktrees.json under both
97
+ # shapes. v11 direct-key: registry keys are worktree names themselves,
98
+ # each value is an entry object with at least { status }. v10 nested:
99
+ # registry.worktrees is the entry map.
100
+ WORKTREES_ACTIVE=false
101
+ PARALLEL_BASE=""
102
+
103
+ if [[ -f "$PROJECT_DIR/.caws/parallel.json" ]] && command -v node >/dev/null 2>&1; then
104
+ PARALLEL_INFO=$(node -e "
105
+ try {
106
+ var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/parallel.json', 'utf8'));
107
+ var agents = (reg.agents || []).length;
108
+ console.log(agents + ':' + (reg.baseBranch || ''));
109
+ } catch(e) { console.log('0:'); }
110
+ " 2>/dev/null || echo "0:")
111
+
112
+ AGENT_COUNT=$(echo "$PARALLEL_INFO" | cut -d: -f1)
113
+ PARALLEL_BASE=$(echo "$PARALLEL_INFO" | cut -d: -f2)
114
+
115
+ if [[ "$AGENT_COUNT" -gt 0 ]] 2>/dev/null; then
116
+ WORKTREES_ACTIVE=true
117
+ fi
118
+ fi
119
+
120
+ if [[ "$WORKTREES_ACTIVE" != "true" ]] && [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>&1; then
121
+ ACTIVE_COUNT=$(node -e "
122
+ try {
123
+ var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
124
+ // Dual-shape: v10 nested vs v11 direct-key.
125
+ function entriesOf(r) {
126
+ if (!r || typeof r !== 'object') return [];
127
+ if (r.worktrees && typeof r.worktrees === 'object') return Object.values(r.worktrees);
128
+ // v11 direct-key: filter to objects with a 'status' field to avoid
129
+ // mistaking top-level metadata (e.g., 'version') for an entry.
130
+ var out = [];
131
+ for (var k in r) {
132
+ if (Object.prototype.hasOwnProperty.call(r, k)) {
133
+ var v = r[k];
134
+ if (v && typeof v === 'object' && typeof v.status === 'string') out.push(v);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ var entries = entriesOf(reg);
140
+ var active = entries.filter(function(w) { return w.status === 'active'; });
141
+ console.log(active.length);
142
+ } catch(e) { console.log('0'); }
143
+ " 2>/dev/null || echo "0")
144
+
145
+ if [[ "$ACTIVE_COUNT" -gt 0 ]] 2>/dev/null; then
146
+ WORKTREES_ACTIVE=true
147
+ fi
148
+ fi
149
+
150
+ if [[ "$WORKTREES_ACTIVE" != "true" ]]; then
151
+ exit 0
152
+ fi
153
+
154
+ # --- Block dangerous git operations when worktrees are active ---
155
+
156
+ if echo "$COMMAND" | grep -qE 'git\s+commit\s+.*--amend'; then
157
+ echo "BLOCKED: git commit --amend is not allowed while worktrees are active." >&2
158
+ echo "Amending commits risks rewriting another agent's work." >&2
159
+ echo "Create a new commit instead." >&2
160
+ exit 2
161
+ fi
162
+
163
+ if echo "$COMMAND" | grep -qE 'git\s+stash' && ! echo "$COMMAND" | grep -qE 'git\s+stash\s+list'; then
164
+ echo "BLOCKED: git stash is not allowed while worktrees are active." >&2
165
+ echo "Stash is shared across all worktrees and can capture or destroy another agent's work." >&2
166
+ echo "Commit your changes to your branch instead." >&2
167
+ exit 2
168
+ fi
169
+
170
+ if echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard'; then
171
+ echo "BLOCKED: git reset --hard is not allowed while worktrees are active." >&2
172
+ echo "This could discard work that other agents depend on." >&2
173
+ exit 2
174
+ fi
175
+
176
+ if echo "$COMMAND" | grep -qE 'git\s+push\s+.*(--force|-f\s)'; then
177
+ echo "BLOCKED: Force push is not allowed while worktrees are active." >&2
178
+ echo "This could rewrite history that other agents have based work on." >&2
179
+ exit 2
180
+ fi
181
+
182
+ # --- Base branch protections ---
183
+ AGENT_DIR="${HOOK_CWD:-${CLAUDE_PROJECT_DIR:-.}}"
184
+ CURRENT_BRANCH=$(cd "$AGENT_DIR" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
185
+
186
+ BASE_BRANCH="$PARALLEL_BASE"
187
+ if [[ -z "$BASE_BRANCH" ]] && [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>&1; then
188
+ BASE_BRANCH=$(node -e "
189
+ try {
190
+ var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
191
+ function entriesOf(r) {
192
+ if (!r || typeof r !== 'object') return [];
193
+ if (r.worktrees && typeof r.worktrees === 'object') return Object.values(r.worktrees);
194
+ var out = [];
195
+ for (var k in r) {
196
+ if (Object.prototype.hasOwnProperty.call(r, k)) {
197
+ var v = r[k];
198
+ if (v && typeof v === 'object' && typeof v.status === 'string') out.push(v);
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+ var entries = entriesOf(reg);
204
+ var active = entries.filter(function(w) { return w.status === 'active'; });
205
+ if (active.length > 0) console.log(active[0].baseBranch || '');
206
+ else console.log('');
207
+ } catch(e) { console.log(''); }
208
+ " 2>/dev/null || echo "")
209
+ fi
210
+
211
+ if [[ -n "$BASE_BRANCH" ]] && [[ "$CURRENT_BRANCH" == "$BASE_BRANCH" ]]; then
212
+ if echo "$COMMAND" | grep -qE 'git\s+push'; then
213
+ echo "BLOCKED: Pushing from the base branch ($BASE_BRANCH) while worktrees are active." >&2
214
+ echo "You should be working in a worktree, not on the base branch." >&2
215
+ echo "Use: cd .caws/worktrees/<name>/" >&2
216
+ exit 2
217
+ fi
218
+
219
+ if echo "$COMMAND" | grep -qE 'git\s+merge\b'; then
220
+ echo '{
221
+ "hookSpecificOutput": {
222
+ "hookEventName": "PreToolUse",
223
+ "additionalContext": "Merging into base branch ('"$BASE_BRANCH"') while worktrees are active. The commit-msg hook will enforce the merge(worktree): message format. Make sure the worktree for this branch has been destroyed first."
224
+ }
225
+ }'
226
+ exit 0
227
+ fi
228
+
229
+ if echo "$COMMAND" | grep -qE 'git\s+commit\b' && ! echo "$COMMAND" | grep -qE '--amend'; then
230
+ echo '{
231
+ "hookSpecificOutput": {
232
+ "hookEventName": "PreToolUse",
233
+ "additionalContext": "NOTE: committing to the base branch ('"$BASE_BRANCH"') while worktrees are active. Worktrees are preferred for isolated feature work, but logical checkpoint commits from the current checkout are allowed by Claude hooks. Avoid --amend and force-push while worktrees are active."
234
+ }
235
+ }'
236
+ exit 0
237
+ fi
238
+ fi
239
+
240
+ exit 0
@@ -0,0 +1,77 @@
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: 4,8,13
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ #
9
+ # CAWS Worktree Write Guard for Claude Code (v11-shape, intentionally
10
+ # fail-open for v11.1).
11
+ #
12
+ # This hook fires on Write/Edit and currently allows all writes from the
13
+ # main checkout. Worktree-first enforcement returns when worktree lifecycle
14
+ # is restored in CLI-WORKTREE-001 (Slice 6). Until then, this hook serves
15
+ # as the managed-install seat for the worktree-write enforcement surface
16
+ # and asserts the always-allowed allowlist so .caws/, .claude/, docs/,
17
+ # scripts/, tmp/, and tests/ writes are never inadvertently blocked by a
18
+ # future enforcement pass that forgets the allowlist.
19
+ #
20
+ # Worktree-active enforcement (when restored) must read the worktrees
21
+ # registry under both shapes:
22
+ # v11 direct-key: { "<name>": { ... } }
23
+ # v10 nested: { "worktrees": { "<name>": { ... } } }
24
+ # and accept both specId (v10) and spec_id (v11) on entries.
25
+
26
+ set -euo pipefail
27
+
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ # shellcheck source=lib/parse-input.sh
30
+ source "$SCRIPT_DIR/lib/parse-input.sh"
31
+ parse_hook_input
32
+
33
+ TOOL_NAME="$HOOK_TOOL_NAME"
34
+ FILE_PATH="$HOOK_FILE_PATH"
35
+
36
+ case "$TOOL_NAME" in
37
+ Write|Edit) ;;
38
+ *) exit 0 ;;
39
+ esac
40
+
41
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
42
+ PROJECT_DIR="$(cd "$PROJECT_DIR" 2>/dev/null && pwd || printf '%s\n' "$PROJECT_DIR")"
43
+
44
+ if command -v git >/dev/null 2>&1; then
45
+ GIT_COMMON_DIR=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
46
+ if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != ".git" ]]; then
47
+ CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON_DIR/.." 2>/dev/null && pwd || echo "")
48
+ if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
49
+ PROJECT_DIR="$CANDIDATE"
50
+ fi
51
+ fi
52
+ fi
53
+
54
+ # Always-allowed paths bypass any future enforcement.
55
+ # User-global Claude state lives outside the repo; .caws/, .claude/, docs/,
56
+ # scripts/, tmp/, .archive/, and .githooks/ are coordination/governance
57
+ # surfaces, not application code.
58
+ if [[ -n "$FILE_PATH" ]]; then
59
+ case "$FILE_PATH" in
60
+ "${HOME:-}"/.claude/*) exit 0 ;;
61
+ "$PROJECT_DIR"/.caws/*|.caws/*) exit 0 ;;
62
+ "$PROJECT_DIR"/.claude/*|.claude/*) exit 0 ;;
63
+ "$PROJECT_DIR"/.gitignore|.gitignore) exit 0 ;;
64
+ "$PROJECT_DIR"/.tmp/*|.tmp/*) exit 0 ;;
65
+ "$PROJECT_DIR"/tmp/*|tmp/*) exit 0 ;;
66
+ "$PROJECT_DIR"/.archive/*|.archive/*) exit 0 ;;
67
+ "$PROJECT_DIR"/.githooks/*|.githooks/*) exit 0 ;;
68
+ "$PROJECT_DIR"/.github/*|.github/*) exit 0 ;;
69
+ "$PROJECT_DIR"/docs/*|docs/*) exit 0 ;;
70
+ esac
71
+ fi
72
+
73
+ # Fail-open until CLI-WORKTREE-001 (Slice 6) restores worktree lifecycle.
74
+ # When that lands, this hook gains worktree-active enforcement using the
75
+ # dual-shape registry helpers that scope-guard.sh and worktree-guard.sh
76
+ # already use.
77
+ exit 0