@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.
- package/README.md +2 -2
- package/dist/index.js +2 -2
- package/dist/init/harness-detect.d.ts +18 -0
- package/dist/init/harness-detect.d.ts.map +1 -0
- package/dist/init/harness-detect.js +90 -0
- package/dist/init/harness-detect.js.map +1 -0
- package/dist/init/hook-install.d.ts +53 -0
- package/dist/init/hook-install.d.ts.map +1 -0
- package/dist/init/hook-install.js +421 -0
- package/dist/init/hook-install.js.map +1 -0
- package/dist/init/hook-packs/manifest-claude-code.d.ts +4 -0
- package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -0
- package/dist/init/hook-packs/manifest-claude-code.js +190 -0
- package/dist/init/hook-packs/manifest-claude-code.js.map +1 -0
- package/dist/init/hook-packs/register.d.ts +19 -0
- package/dist/init/hook-packs/register.d.ts.map +1 -0
- package/dist/init/hook-packs/register.js +37 -0
- package/dist/init/hook-packs/register.js.map +1 -0
- package/dist/init/hook-packs/types.d.ts +123 -0
- package/dist/init/hook-packs/types.d.ts.map +1 -0
- package/dist/init/hook-packs/types.js +29 -0
- package/dist/init/hook-packs/types.js.map +1 -0
- package/dist/shell/commands/gates.d.ts.map +1 -1
- package/dist/shell/commands/gates.js +28 -1
- package/dist/shell/commands/gates.js.map +1 -1
- package/dist/shell/commands/init.d.ts +9 -0
- package/dist/shell/commands/init.d.ts.map +1 -1
- package/dist/shell/commands/init.js +131 -27
- package/dist/shell/commands/init.js.map +1 -1
- package/dist/shell/commands/specs.d.ts +41 -0
- package/dist/shell/commands/specs.d.ts.map +1 -0
- package/dist/shell/commands/specs.js +264 -0
- package/dist/shell/commands/specs.js.map +1 -0
- package/dist/shell/commands/worktree.d.ts +38 -0
- package/dist/shell/commands/worktree.d.ts.map +1 -0
- package/dist/shell/commands/worktree.js +286 -0
- package/dist/shell/commands/worktree.js.map +1 -0
- package/dist/shell/gates/disposition.d.ts.map +1 -1
- package/dist/shell/gates/disposition.js +33 -3
- package/dist/shell/gates/disposition.js.map +1 -1
- package/dist/shell/gates/local-evaluators/budget-limit.d.ts +24 -0
- package/dist/shell/gates/local-evaluators/budget-limit.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/budget-limit.js +67 -0
- package/dist/shell/gates/local-evaluators/budget-limit.js.map +1 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.d.ts +25 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.js +74 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.js.map +1 -0
- package/dist/shell/gates/local-evaluators/index.d.ts +28 -0
- package/dist/shell/gates/local-evaluators/index.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/index.js +67 -0
- package/dist/shell/gates/local-evaluators/index.js.map +1 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.d.ts +23 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.js +67 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.js.map +1 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.d.ts +12 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.js +73 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.js.map +1 -0
- package/dist/shell/index.d.ts +4 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +13 -1
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/register.d.ts.map +1 -1
- package/dist/shell/register.js +192 -2
- package/dist/shell/register.js.map +1 -1
- package/dist/shell/render/init-hook-pack.d.ts +16 -0
- package/dist/shell/render/init-hook-pack.d.ts.map +1 -0
- package/dist/shell/render/init-hook-pack.js +206 -0
- package/dist/shell/render/init-hook-pack.js.map +1 -0
- package/dist/store/atomic-write.d.ts +20 -2
- package/dist/store/atomic-write.d.ts.map +1 -1
- package/dist/store/atomic-write.js +44 -2
- package/dist/store/atomic-write.js.map +1 -1
- package/dist/store/lifecycle-lock.d.ts +34 -0
- package/dist/store/lifecycle-lock.d.ts.map +1 -0
- package/dist/store/lifecycle-lock.js +168 -0
- package/dist/store/lifecycle-lock.js.map +1 -0
- package/dist/store/lifecycle-transaction.d.ts +79 -0
- package/dist/store/lifecycle-transaction.d.ts.map +1 -0
- package/dist/store/lifecycle-transaction.js +319 -0
- package/dist/store/lifecycle-transaction.js.map +1 -0
- package/dist/store/rules.d.ts +16 -0
- package/dist/store/rules.d.ts.map +1 -1
- package/dist/store/rules.js +17 -0
- package/dist/store/rules.js.map +1 -1
- package/dist/store/specs-writer.d.ts +61 -0
- package/dist/store/specs-writer.d.ts.map +1 -0
- package/dist/store/specs-writer.js +506 -0
- package/dist/store/specs-writer.js.map +1 -0
- package/dist/store/worktrees-writer.d.ts +77 -0
- package/dist/store/worktrees-writer.d.ts.map +1 -0
- package/dist/store/worktrees-writer.js +674 -0
- package/dist/store/worktrees-writer.js.map +1 -0
- package/dist/store/yaml-patch.d.ts +7 -0
- package/dist/store/yaml-patch.d.ts.map +1 -0
- package/dist/store/yaml-patch.js +250 -0
- package/dist/store/yaml-patch.js.map +1 -0
- package/package.json +7 -4
- 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
|
@@ -0,0 +1,63 @@
|
|
|
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: 8,16
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# PostToolUse dispatcher for Claude Code hooks.
|
|
9
|
+
#
|
|
10
|
+
# Single entry point invoked from settings.json's PostToolUse block. Reads
|
|
11
|
+
# stdin ONCE, sanitizes via lib/parse-input.sh, then invokes every
|
|
12
|
+
# registered handler with HOOK_* env vars inherited and the sanitized
|
|
13
|
+
# JSON piped to each handler's stdin.
|
|
14
|
+
#
|
|
15
|
+
# Differences from pre_tool_use.sh:
|
|
16
|
+
# - HANDLERS entries may carry a positional argument (e.g. "audit.sh
|
|
17
|
+
# tool-use"). Entries are split on whitespace and passed to the
|
|
18
|
+
# handler as argv, so existing scripts that dispatch on $1 keep
|
|
19
|
+
# working without change.
|
|
20
|
+
# - Exit 2 is a no-op for PostToolUse semantically (the tool has
|
|
21
|
+
# already run) but we still honor it to short-circuit the chain and
|
|
22
|
+
# propagate the blocker's stderr, matching the pre-tool-use
|
|
23
|
+
# contract.
|
|
24
|
+
#
|
|
25
|
+
# Stdout: last non-empty handler buffer wins. Most PostToolUse handlers
|
|
26
|
+
# write hookSpecificOutput JSON (quality-check, validate-spec, naming,
|
|
27
|
+
# doc-frontmatter). Since each of those self-filters on file type, only
|
|
28
|
+
# one of them emits stdout for any given Write/Edit. If two ever collide
|
|
29
|
+
# (e.g., a YAML file that happens to match both the spec validator and
|
|
30
|
+
# the naming check), the later-in-HANDLERS wins. Order below is set so
|
|
31
|
+
# the more informative check runs last.
|
|
32
|
+
#
|
|
33
|
+
# Stderr: prefixed with "[<handler>]" so the source of any message is
|
|
34
|
+
# visible to the agent.
|
|
35
|
+
#
|
|
36
|
+
# Fail-open: parser or lib failure returns exit 0 silently.
|
|
37
|
+
|
|
38
|
+
set -uo pipefail
|
|
39
|
+
|
|
40
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
41
|
+
HOOKS_DIR="$(dirname "$SCRIPT_DIR")"
|
|
42
|
+
|
|
43
|
+
# shellcheck source=../lib/parse-input.sh
|
|
44
|
+
source "$HOOKS_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
|
|
45
|
+
parse_hook_input || exit 0
|
|
46
|
+
|
|
47
|
+
# shellcheck source=../lib/run-handlers.sh
|
|
48
|
+
source "$HOOKS_DIR/lib/run-handlers.sh" 2>/dev/null || exit 0
|
|
49
|
+
|
|
50
|
+
# Registered handlers in execution order. Mirrors the pre-registry
|
|
51
|
+
# settings.json groups so ordering-sensitive behavior (stdout "last
|
|
52
|
+
# wins" policy, audit log ordering) is preserved.
|
|
53
|
+
HANDLERS=(
|
|
54
|
+
# "quality-check.sh"
|
|
55
|
+
# "validate-spec.sh"
|
|
56
|
+
# "naming-check.sh"
|
|
57
|
+
# "doc-frontmatter-check.sh"
|
|
58
|
+
# "audit.sh tool-use"
|
|
59
|
+
# "plan-transcript-snapshot.sh"
|
|
60
|
+
"session-log.sh"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
run_handlers "${HANDLERS[@]}"
|
|
@@ -0,0 +1,50 @@
|
|
|
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: 8,11,17
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
#
|
|
9
|
+
# PreToolUse dispatcher for Claude Code hooks.
|
|
10
|
+
#
|
|
11
|
+
# Single entry point invoked from settings.json's PreToolUse block. Reads
|
|
12
|
+
# stdin ONCE, sanitizes it via lib/parse-input.sh, then invokes every
|
|
13
|
+
# registered handler with HOOK_* env vars inherited and the sanitized
|
|
14
|
+
# JSON piped to each handler's stdin.
|
|
15
|
+
#
|
|
16
|
+
# Handlers self-filter via their own matcher predicate (a case statement
|
|
17
|
+
# on $HOOK_TOOL_NAME at the top of the script).
|
|
18
|
+
#
|
|
19
|
+
# Exit-code aggregation:
|
|
20
|
+
# - First handler exiting 2 short-circuits the remaining handlers and
|
|
21
|
+
# the dispatcher returns 2 (blocking).
|
|
22
|
+
# - Non-zero non-2 exits are warnings; the dispatcher continues and
|
|
23
|
+
# returns the max non-2 code at the end.
|
|
24
|
+
#
|
|
25
|
+
# Fail-open: if the dispatcher itself errors before any handler runs
|
|
26
|
+
# (parser crash, missing lib), it exits 0 rather than blocking the tool.
|
|
27
|
+
# Guard infrastructure must not turn its own bugs into tool-call blocks.
|
|
28
|
+
|
|
29
|
+
set -uo pipefail
|
|
30
|
+
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
HOOKS_DIR="$(dirname "$SCRIPT_DIR")"
|
|
33
|
+
|
|
34
|
+
# shellcheck source=../lib/parse-input.sh
|
|
35
|
+
source "$HOOKS_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
|
|
36
|
+
parse_hook_input || exit 0
|
|
37
|
+
|
|
38
|
+
# shellcheck source=../lib/run-handlers.sh
|
|
39
|
+
source "$HOOKS_DIR/lib/run-handlers.sh" 2>/dev/null || exit 0
|
|
40
|
+
|
|
41
|
+
# Registered handlers in execution order. Each handler self-filters
|
|
42
|
+
# on $HOOK_TOOL_NAME; non-matching cases return exit 0 cheaply.
|
|
43
|
+
HANDLERS=(
|
|
44
|
+
block-dangerous.sh
|
|
45
|
+
worktree-guard.sh
|
|
46
|
+
scope-guard.sh
|
|
47
|
+
worktree-write-guard.sh
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
run_handlers --short-circuit-on-block "${HANDLERS[@]}"
|
|
@@ -0,0 +1,41 @@
|
|
|
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,11
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# SessionStart dispatcher for Claude Code hooks.
|
|
9
|
+
#
|
|
10
|
+
# Fires once per session. Same fan-out semantics as pre_tool_use.sh and
|
|
11
|
+
# post_tool_use.sh: reads stdin once, exports HOOK_* env vars, invokes
|
|
12
|
+
# each registered handler with stdin piped and stderr prefixed.
|
|
13
|
+
#
|
|
14
|
+
# HANDLERS entries may carry a positional argument (e.g. "audit.sh
|
|
15
|
+
# session-start" -- audit.sh's event type is an argv, not a field in
|
|
16
|
+
# the stdin payload). Entries are split on whitespace and passed as argv.
|
|
17
|
+
#
|
|
18
|
+
# SessionStart semantics: these hooks inform the agent about session
|
|
19
|
+
# state (CAWS briefing, audit log bootstrap, session-log meta file).
|
|
20
|
+
# None should block. Exit 2 is treated the same as exit 1 here --
|
|
21
|
+
# recorded as max_exit but does not short-circuit.
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
HOOKS_DIR="$(dirname "$SCRIPT_DIR")"
|
|
27
|
+
|
|
28
|
+
# shellcheck source=../lib/parse-input.sh
|
|
29
|
+
source "$HOOKS_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
|
|
30
|
+
parse_hook_input || exit 0
|
|
31
|
+
|
|
32
|
+
# shellcheck source=../lib/run-handlers.sh
|
|
33
|
+
source "$HOOKS_DIR/lib/run-handlers.sh" 2>/dev/null || exit 0
|
|
34
|
+
|
|
35
|
+
HANDLERS=(
|
|
36
|
+
"audit.sh session-start"
|
|
37
|
+
# "session-caws-status.sh session-start"
|
|
38
|
+
"session-log.sh"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
run_handlers "${HANDLERS[@]}"
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
# Stop dispatcher for Claude Code hooks.
|
|
9
|
+
#
|
|
10
|
+
# Fires at end of session. Same fan-out semantics as the other dispatchers.
|
|
11
|
+
# Handlers here finalize session artifacts: audit log closeout, worktree
|
|
12
|
+
# cleanup reminder, plan-transcript finalize, session-log handoff.
|
|
13
|
+
#
|
|
14
|
+
# Stop semantics: none of these handlers should block the user -- the
|
|
15
|
+
# session is already ending. All non-zero exits are treated as warnings;
|
|
16
|
+
# max_exit is reported but no handler short-circuits the chain.
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
HOOKS_DIR="$(dirname "$SCRIPT_DIR")"
|
|
22
|
+
|
|
23
|
+
# shellcheck source=../lib/parse-input.sh
|
|
24
|
+
source "$HOOKS_DIR/lib/parse-input.sh" 2>/dev/null || exit 0
|
|
25
|
+
parse_hook_input || exit 0
|
|
26
|
+
|
|
27
|
+
# shellcheck source=../lib/run-handlers.sh
|
|
28
|
+
source "$HOOKS_DIR/lib/run-handlers.sh" 2>/dev/null || exit 0
|
|
29
|
+
|
|
30
|
+
HANDLERS=(
|
|
31
|
+
# "audit.sh stop"
|
|
32
|
+
# "stop-worktree-check.sh"
|
|
33
|
+
"plan-transcript-finalize.sh"
|
|
34
|
+
"session-log.sh"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
run_handlers "${HANDLERS[@]}"
|
|
@@ -0,0 +1,140 @@
|
|
|
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: 8,16
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# Shared progressive strike handling for Claude pre-write guard hooks.
|
|
9
|
+
#
|
|
10
|
+
# If you are reading this because a guard blocked you, do not edit this file or
|
|
11
|
+
# the generated guard-strikes JSON directly to bypass enforcement. The correct
|
|
12
|
+
# recovery paths are:
|
|
13
|
+
# 1. Switch into the right CAWS worktree.
|
|
14
|
+
# 2. Bring the target file into the active spec's scope.in (if it legitimately
|
|
15
|
+
# belongs there), then ask the user to reset your strikes by running:
|
|
16
|
+
# bash .claude/hooks/reset-strikes.sh --current
|
|
17
|
+
# or the equivalent narrower reset (see --help).
|
|
18
|
+
# 3. Ask the user to resolve the conflict explicitly.
|
|
19
|
+
# Never edit guard-strikes-*.json files by hand — use reset-strikes.sh so the
|
|
20
|
+
# reason is logged to .claude/logs/strike-resets.log.
|
|
21
|
+
|
|
22
|
+
guard_worktree_state_dir() {
|
|
23
|
+
local cwd_hint="${1:-}"
|
|
24
|
+
local project_dir="${CLAUDE_PROJECT_DIR:-.}"
|
|
25
|
+
local worktree_dir=""
|
|
26
|
+
|
|
27
|
+
if [[ -n "$cwd_hint" ]] && [[ "$cwd_hint" =~ ^(.*\/\.caws\/worktrees\/[^/]+)($|/) ]]; then
|
|
28
|
+
worktree_dir="${BASH_REMATCH[1]}"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [[ -z "$worktree_dir" ]] && [[ "$project_dir" =~ ^(.*\/\.caws\/worktrees\/[^/]+)($|/) ]]; then
|
|
32
|
+
worktree_dir="${BASH_REMATCH[1]}"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [[ -n "$worktree_dir" ]] && [[ -d "$worktree_dir" ]]; then
|
|
36
|
+
mkdir -p "$worktree_dir/tmp"
|
|
37
|
+
printf '%s\n' "$worktree_dir/tmp"
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
guard_strikes_file() {
|
|
45
|
+
local project_dir="${CLAUDE_PROJECT_DIR:-.}"
|
|
46
|
+
local cwd_hint="${2:-}"
|
|
47
|
+
local log_dir="$project_dir/.claude/logs"
|
|
48
|
+
local session_id="$1"
|
|
49
|
+
local safe_session
|
|
50
|
+
|
|
51
|
+
if guard_worktree_state_dir "$cwd_hint" >/dev/null 2>&1; then
|
|
52
|
+
log_dir=$(guard_worktree_state_dir "$cwd_hint")
|
|
53
|
+
else
|
|
54
|
+
mkdir -p "$log_dir"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
safe_session=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
|
58
|
+
printf '%s/guard-strikes-%s.json' "$log_dir" "$safe_session"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
guard_record_strike() {
|
|
62
|
+
local session_id="$1"
|
|
63
|
+
local guard_name="$2"
|
|
64
|
+
local cwd_hint="${3:-}"
|
|
65
|
+
local state_file
|
|
66
|
+
local current_count
|
|
67
|
+
|
|
68
|
+
state_file=$(guard_strikes_file "$session_id" "$cwd_hint")
|
|
69
|
+
if [[ ! -f "$state_file" ]]; then
|
|
70
|
+
printf '{}\n' > "$state_file"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
current_count=$(jq -r --arg guard "$guard_name" '.[$guard] // 0' "$state_file" 2>/dev/null || printf '0')
|
|
74
|
+
if [[ ! "$current_count" =~ ^[0-9]+$ ]]; then
|
|
75
|
+
current_count=0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
current_count=$((current_count + 1))
|
|
79
|
+
|
|
80
|
+
jq --arg guard "$guard_name" --argjson count "$current_count" '.[$guard] = $count' "$state_file" > "$state_file.tmp"
|
|
81
|
+
mv "$state_file.tmp" "$state_file"
|
|
82
|
+
|
|
83
|
+
printf '%s\n' "$current_count"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
guard_emit_warning_allow() {
|
|
87
|
+
local message="$1"
|
|
88
|
+
|
|
89
|
+
jq -n --arg msg "$message" '{
|
|
90
|
+
hookSpecificOutput: {
|
|
91
|
+
hookEventName: "PreToolUse",
|
|
92
|
+
additionalContext: $msg
|
|
93
|
+
}
|
|
94
|
+
}'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
guard_emit_permission_ask() {
|
|
98
|
+
local message="$1"
|
|
99
|
+
|
|
100
|
+
jq -n --arg msg "$message" '{
|
|
101
|
+
hookSpecificOutput: {
|
|
102
|
+
hookEventName: "PreToolUse",
|
|
103
|
+
permissionDecision: "ask",
|
|
104
|
+
permissionDecisionReason: $msg
|
|
105
|
+
}
|
|
106
|
+
}'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
guard_emit_block() {
|
|
110
|
+
local message="$1"
|
|
111
|
+
|
|
112
|
+
jq -n --arg msg "$message" '{
|
|
113
|
+
decision: "block",
|
|
114
|
+
reason: $msg
|
|
115
|
+
}'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
guard_enforce_progressive_strikes() {
|
|
119
|
+
local session_id="$1"
|
|
120
|
+
local guard_name="$2"
|
|
121
|
+
local cwd_hint="$3"
|
|
122
|
+
local first_message="$4"
|
|
123
|
+
local second_message="$5"
|
|
124
|
+
local third_message="$6"
|
|
125
|
+
local strike
|
|
126
|
+
|
|
127
|
+
strike=$(guard_record_strike "$session_id" "$guard_name" "$cwd_hint")
|
|
128
|
+
|
|
129
|
+
case "$strike" in
|
|
130
|
+
1)
|
|
131
|
+
guard_emit_warning_allow "$first_message"
|
|
132
|
+
;;
|
|
133
|
+
2)
|
|
134
|
+
guard_emit_permission_ask "$second_message"
|
|
135
|
+
;;
|
|
136
|
+
*)
|
|
137
|
+
guard_emit_block "$third_message"
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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: 8,16
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# Shared hook input parser for Claude Code hooks.
|
|
9
|
+
#
|
|
10
|
+
# Handlers source this file and call `parse_hook_input` to get HOOK_* env
|
|
11
|
+
# vars populated from the tool-call payload. This replaces the per-handler
|
|
12
|
+
# pattern of `INPUT=$(read_hook_input_json)` followed by 3-5 `jq` calls.
|
|
13
|
+
#
|
|
14
|
+
# Two invocation modes:
|
|
15
|
+
# 1. Standalone handler (legacy): reads stdin via read_hook_input_json,
|
|
16
|
+
# parses, exports HOOK_*.
|
|
17
|
+
# 2. Via dispatcher (planned Phase 2): HOOK_INPUT_JSON already exported
|
|
18
|
+
# by the router; parse_hook_input just re-extracts scalar fields.
|
|
19
|
+
#
|
|
20
|
+
# Why one shared parser: before this lib, each of 17 handlers independently
|
|
21
|
+
# ran read_hook_input_json + 3-5 jq calls on the same payload. A bug in the
|
|
22
|
+
# parser (like the control-char failure fixed in runtime-paths.sh) ripples
|
|
23
|
+
# to every handler. One parser, one bug surface, one fix.
|
|
24
|
+
#
|
|
25
|
+
# Idempotent source: safe to source multiple times.
|
|
26
|
+
|
|
27
|
+
if [[ -n "${_HOOK_PARSE_INPUT_LOADED:-}" ]]; then
|
|
28
|
+
return 0 2>/dev/null || exit 0
|
|
29
|
+
fi
|
|
30
|
+
_HOOK_PARSE_INPUT_LOADED=1
|
|
31
|
+
|
|
32
|
+
_hook_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
33
|
+
# shellcheck source=../runtime-paths.sh
|
|
34
|
+
source "$_hook_lib_dir/../runtime-paths.sh"
|
|
35
|
+
|
|
36
|
+
parse_hook_input() {
|
|
37
|
+
# Fast path: the dispatcher already parsed the input and exported
|
|
38
|
+
# HOOK_* env vars to the handler's environment. Re-extracting from
|
|
39
|
+
# HOOK_INPUT_JSON would be a wasted python subprocess. HOOK_TOOL_NAME
|
|
40
|
+
# is the canonical "parse completed" marker -- after a completed parse
|
|
41
|
+
# it's always defined (possibly empty for malformed input), so the
|
|
42
|
+
# `${HOOK_TOOL_NAME+set}` test distinguishes "parser ran" from
|
|
43
|
+
# "handler invoked standalone and parser hasn't run yet".
|
|
44
|
+
if [[ -n "${HOOK_TOOL_NAME+set}" ]]; then
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# If HOOK_INPUT_JSON is set but HOOK_TOOL_NAME is not, a caller staged
|
|
49
|
+
# the sanitized payload but didn't run the extractor. Extract now.
|
|
50
|
+
# Otherwise (standalone handler), read stdin via the sanitizer.
|
|
51
|
+
if [[ -z "${HOOK_INPUT_JSON:-}" ]]; then
|
|
52
|
+
HOOK_INPUT_JSON="$(read_hook_input_json)"
|
|
53
|
+
export HOOK_INPUT_JSON
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Extract all common scalar fields in ONE python call, emitting
|
|
57
|
+
# shlex-quoted bash assignments. Compared to 3-5 separate `jq` calls,
|
|
58
|
+
# this is one subprocess per handler instead of many. Values are sh-safe
|
|
59
|
+
# via shlex.quote, so `eval` is not a code-injection hazard.
|
|
60
|
+
local assignments
|
|
61
|
+
assignments=$(printf '%s' "$HOOK_INPUT_JSON" | python3 -c '
|
|
62
|
+
import json
|
|
63
|
+
import shlex
|
|
64
|
+
import sys
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(sys.stdin.read() or "{}")
|
|
68
|
+
except Exception:
|
|
69
|
+
data = {}
|
|
70
|
+
if not isinstance(data, dict):
|
|
71
|
+
data = {}
|
|
72
|
+
|
|
73
|
+
tool_input = data.get("tool_input")
|
|
74
|
+
if not isinstance(tool_input, dict):
|
|
75
|
+
tool_input = {}
|
|
76
|
+
|
|
77
|
+
tool_response = data.get("tool_response")
|
|
78
|
+
if not isinstance(tool_response, dict):
|
|
79
|
+
tool_response = {}
|
|
80
|
+
|
|
81
|
+
fields = {
|
|
82
|
+
"HOOK_TOOL_NAME": data.get("tool_name") or "",
|
|
83
|
+
"HOOK_FILE_PATH": tool_input.get("file_path") or "",
|
|
84
|
+
"HOOK_COMMAND": tool_input.get("command") or "",
|
|
85
|
+
"HOOK_CWD": data.get("cwd") or "",
|
|
86
|
+
"HOOK_SESSION_ID": data.get("session_id") or "unknown",
|
|
87
|
+
"HOOK_TRANSCRIPT_PATH": data.get("transcript_path") or "",
|
|
88
|
+
"HOOK_EVENT_NAME": data.get("hook_event_name") or "",
|
|
89
|
+
"HOOK_MODEL": data.get("model") or "",
|
|
90
|
+
"HOOK_SOURCE": data.get("source") or "",
|
|
91
|
+
"HOOK_PERMISSION_MODE": data.get("permission_mode") or "default",
|
|
92
|
+
"HOOK_TOOL_USE_ID": data.get("tool_use_id") or "",
|
|
93
|
+
"HOOK_STOP_HOOK_ACTIVE": "1" if data.get("stop_hook_active") else "0",
|
|
94
|
+
# Whole objects as JSON strings -- consumed by audit.sh for log payloads.
|
|
95
|
+
# Always valid JSON ("{}" at minimum) so `jq --argjson` works without
|
|
96
|
+
# a defensive check in every caller.
|
|
97
|
+
"HOOK_TOOL_INPUT_JSON": json.dumps(tool_input),
|
|
98
|
+
"HOOK_TOOL_RESPONSE_JSON": json.dumps(tool_response),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for k, v in fields.items():
|
|
102
|
+
print(f"{k}={shlex.quote(str(v))}")
|
|
103
|
+
' 2>/dev/null || true)
|
|
104
|
+
|
|
105
|
+
# Fail-open: if the python subprocess failed for any reason, leave
|
|
106
|
+
# HOOK_* vars unset/empty. Handlers will see empty tool_name and
|
|
107
|
+
# short-circuit on their own matcher predicate. Guard infrastructure
|
|
108
|
+
# must never block a tool call because its parser crashed.
|
|
109
|
+
if [[ -n "$assignments" ]]; then
|
|
110
|
+
eval "$assignments"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
export HOOK_TOOL_NAME="${HOOK_TOOL_NAME:-}" \
|
|
114
|
+
HOOK_FILE_PATH="${HOOK_FILE_PATH:-}" \
|
|
115
|
+
HOOK_COMMAND="${HOOK_COMMAND:-}" \
|
|
116
|
+
HOOK_CWD="${HOOK_CWD:-}" \
|
|
117
|
+
HOOK_SESSION_ID="${HOOK_SESSION_ID:-unknown}" \
|
|
118
|
+
HOOK_TRANSCRIPT_PATH="${HOOK_TRANSCRIPT_PATH:-}" \
|
|
119
|
+
HOOK_EVENT_NAME="${HOOK_EVENT_NAME:-}" \
|
|
120
|
+
HOOK_MODEL="${HOOK_MODEL:-}" \
|
|
121
|
+
HOOK_SOURCE="${HOOK_SOURCE:-}" \
|
|
122
|
+
HOOK_PERMISSION_MODE="${HOOK_PERMISSION_MODE:-default}" \
|
|
123
|
+
HOOK_TOOL_USE_ID="${HOOK_TOOL_USE_ID:-}" \
|
|
124
|
+
HOOK_STOP_HOOK_ACTIVE="${HOOK_STOP_HOOK_ACTIVE:-0}" \
|
|
125
|
+
HOOK_TOOL_INPUT_JSON="${HOOK_TOOL_INPUT_JSON:-{\}}" \
|
|
126
|
+
HOOK_TOOL_RESPONSE_JSON="${HOOK_TOOL_RESPONSE_JSON:-{\}}"
|
|
127
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
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: 8,16
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# Shared handler-dispatch loop for Claude Code hook dispatchers.
|
|
9
|
+
#
|
|
10
|
+
# Source this file from a dispatcher script, then call:
|
|
11
|
+
#
|
|
12
|
+
# run_handlers [--short-circuit-on-block] <handler-entry>...
|
|
13
|
+
#
|
|
14
|
+
# Each handler-entry is a whitespace-separated string whose first token is
|
|
15
|
+
# the handler script filename (relative to HOOKS_DIR) and whose remaining
|
|
16
|
+
# tokens are positional arguments forwarded to that script. Example:
|
|
17
|
+
#
|
|
18
|
+
# run_handlers "cwd-guard.sh" "audit.sh tool-use" "session-log.sh"
|
|
19
|
+
#
|
|
20
|
+
# The caller must set HOOKS_DIR before sourcing this file (the dispatcher
|
|
21
|
+
# boilerplate does this already).
|
|
22
|
+
#
|
|
23
|
+
# Environment variables consumed:
|
|
24
|
+
# HOOK_INPUT_JSON — the sanitized JSON payload piped to every handler.
|
|
25
|
+
# If not yet set, parse_hook_input is called to
|
|
26
|
+
# populate it along with all HOOK_* scalar vars.
|
|
27
|
+
# CLAUDE_HOOK_DRY_RUN — if non-empty and non-zero, still invoke every
|
|
28
|
+
# handler but always return 0 from run_handlers and
|
|
29
|
+
# emit "[DRY-RUN] <handler>.sh would have exited <N>"
|
|
30
|
+
# to stderr for any non-zero exit.
|
|
31
|
+
# CLAUDE_HOOK_TIMING — if non-empty and non-zero, emit
|
|
32
|
+
# "[timing] <handler>.sh: <N>ms" to stderr after
|
|
33
|
+
# each handler invocation. Does not affect exit codes
|
|
34
|
+
# or stdout behavior.
|
|
35
|
+
#
|
|
36
|
+
# Stdout: the last non-empty buffer written to a handler's stdout is forwarded
|
|
37
|
+
# to run_handlers' caller's stdout ("last wins").
|
|
38
|
+
#
|
|
39
|
+
# Return value: the maximum exit code across all handlers (or 2 immediately if
|
|
40
|
+
# --short-circuit-on-block is set and any handler exits 2). When
|
|
41
|
+
# CLAUDE_HOOK_DRY_RUN is set the effective return is always 0.
|
|
42
|
+
#
|
|
43
|
+
# Idempotent source: safe to source multiple times.
|
|
44
|
+
|
|
45
|
+
if [[ -n "${_HOOK_RUN_HANDLERS_LOADED:-}" ]]; then
|
|
46
|
+
return 0 2>/dev/null || exit 0
|
|
47
|
+
fi
|
|
48
|
+
_HOOK_RUN_HANDLERS_LOADED=1
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# _rh_is_truthy <value>
|
|
52
|
+
# Returns 0 (true) when value is non-empty and not "0".
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
_rh_is_truthy() {
|
|
55
|
+
local val="${1:-}"
|
|
56
|
+
[[ -n "$val" && "$val" != "0" ]]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# _rh_ms_now
|
|
61
|
+
# Prints current Unix time in milliseconds (integer).
|
|
62
|
+
# Uses date +%s%N if available, falls back to python3.
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
_rh_ms_now() {
|
|
65
|
+
local ns
|
|
66
|
+
ns=$(date +%s%N 2>/dev/null)
|
|
67
|
+
# macOS date does not support %N; it prints literally "%N"
|
|
68
|
+
if [[ "$ns" == *%N* ]]; then
|
|
69
|
+
python3 -c 'import time; print(int(time.time() * 1000))'
|
|
70
|
+
else
|
|
71
|
+
printf '%d\n' "$(( ns / 1000000 ))"
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_rh_stdout_priority() {
|
|
76
|
+
local payload="$1"
|
|
77
|
+
local decision
|
|
78
|
+
decision=$(printf '%s' "$payload" | jq -r '.decision // .hookSpecificOutput.permissionDecision // ""' 2>/dev/null || true)
|
|
79
|
+
case "$decision" in
|
|
80
|
+
block) printf '3\n' ;;
|
|
81
|
+
ask) printf '2\n' ;;
|
|
82
|
+
*) printf '1\n' ;;
|
|
83
|
+
esac
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# run_handlers [--short-circuit-on-block] <handler-entry>...
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
run_handlers() {
|
|
90
|
+
local short_circuit=0
|
|
91
|
+
if [[ "${1:-}" == "--short-circuit-on-block" ]]; then
|
|
92
|
+
short_circuit=1
|
|
93
|
+
shift
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Ensure the input is parsed and HOOK_INPUT_JSON is available.
|
|
97
|
+
# parse-input.sh is idempotent (guarded by _HOOK_PARSE_INPUT_LOADED).
|
|
98
|
+
if [[ -z "${HOOK_INPUT_JSON:-}" ]]; then
|
|
99
|
+
# HOOKS_DIR must be set by the caller (dispatcher boilerplate).
|
|
100
|
+
# shellcheck source=parse-input.sh
|
|
101
|
+
source "${HOOKS_DIR}/lib/parse-input.sh" 2>/dev/null || return 0
|
|
102
|
+
parse_hook_input || return 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
local dry_run=0
|
|
106
|
+
_rh_is_truthy "${CLAUDE_HOOK_DRY_RUN:-}" && dry_run=1
|
|
107
|
+
|
|
108
|
+
local timing=0
|
|
109
|
+
_rh_is_truthy "${CLAUDE_HOOK_TIMING:-}" && timing=1
|
|
110
|
+
|
|
111
|
+
local max_exit=0
|
|
112
|
+
local last_stdout=""
|
|
113
|
+
local last_stdout_priority=0
|
|
114
|
+
|
|
115
|
+
# Snapshot the outer $@ into an array so `set --` inside the loop can safely
|
|
116
|
+
# clobber positional params without breaking iteration. Using "$@" directly
|
|
117
|
+
# with `for entry in "$@"` captures at loop start on modern bash, but this
|
|
118
|
+
# is safer across shells and makes the intent explicit.
|
|
119
|
+
local entries
|
|
120
|
+
entries=("$@")
|
|
121
|
+
|
|
122
|
+
local entry
|
|
123
|
+
for entry in "${entries[@]}"; do
|
|
124
|
+
# Split on whitespace: first token = script, rest = positional args.
|
|
125
|
+
# shellcheck disable=SC2086
|
|
126
|
+
set -- $entry
|
|
127
|
+
local handler="$1"
|
|
128
|
+
shift
|
|
129
|
+
# "$@" now holds the handler's positional args (may be empty). Use it
|
|
130
|
+
# directly rather than stashing into a local array -- bash 3.2 (macOS
|
|
131
|
+
# default) has quirky ${arr[@]+"${arr[@]}"} expansion behavior for
|
|
132
|
+
# empty arrays under set -u in certain command-substitution contexts.
|
|
133
|
+
# "$@" has no such quirks: empty positional params under set -u is a
|
|
134
|
+
# normal, non-error case.
|
|
135
|
+
|
|
136
|
+
local handler_path="${HOOKS_DIR}/${handler}"
|
|
137
|
+
if [[ ! -x "$handler_path" ]]; then
|
|
138
|
+
continue
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
local t_start=0
|
|
142
|
+
if (( timing )); then
|
|
143
|
+
t_start=$(_rh_ms_now)
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
local stderr_file
|
|
147
|
+
stderr_file=$(mktemp)
|
|
148
|
+
local stdout_buf
|
|
149
|
+
stdout_buf=$(printf '%s' "$HOOK_INPUT_JSON" \
|
|
150
|
+
| "$handler_path" "$@" 2>"$stderr_file")
|
|
151
|
+
local exit_code=$?
|
|
152
|
+
|
|
153
|
+
local t_elapsed=0
|
|
154
|
+
if (( timing )); then
|
|
155
|
+
local t_end
|
|
156
|
+
t_end=$(_rh_ms_now)
|
|
157
|
+
t_elapsed=$(( t_end - t_start ))
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Re-emit handler stderr prefixed with handler name.
|
|
161
|
+
if [[ -s "$stderr_file" ]]; then
|
|
162
|
+
while IFS= read -r line; do
|
|
163
|
+
printf '[%s] %s\n' "$handler" "$line" >&2
|
|
164
|
+
done < "$stderr_file"
|
|
165
|
+
fi
|
|
166
|
+
rm -f "$stderr_file"
|
|
167
|
+
|
|
168
|
+
# Timing annotation (after handler stderr so they don't interleave).
|
|
169
|
+
if (( timing )); then
|
|
170
|
+
printf '[timing] %s: %dms\n' "$handler" "$t_elapsed" >&2
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Dry-run annotation for non-zero exits.
|
|
174
|
+
if (( dry_run )) && (( exit_code != 0 )); then
|
|
175
|
+
printf '[DRY-RUN] %s would have exited %d\n' "$handler" "$exit_code" >&2
|
|
176
|
+
exit_code=0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Accumulate stdout. Structured block/ask decisions outrank lower-priority
|
|
180
|
+
# hook context so a later handler cannot accidentally erase a safety
|
|
181
|
+
# boundary emitted by an earlier handler.
|
|
182
|
+
if [[ -n "$stdout_buf" ]]; then
|
|
183
|
+
local stdout_priority
|
|
184
|
+
stdout_priority=$(_rh_stdout_priority "$stdout_buf")
|
|
185
|
+
if [[ "$stdout_priority" -eq 3 ]]; then
|
|
186
|
+
printf '%s\n' "$stdout_buf"
|
|
187
|
+
return 2
|
|
188
|
+
fi
|
|
189
|
+
if [[ "$stdout_priority" -ge "$last_stdout_priority" ]]; then
|
|
190
|
+
last_stdout="$stdout_buf"
|
|
191
|
+
last_stdout_priority="$stdout_priority"
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# Short-circuit on blocking exit (exit 2), unless dry-run zeroed it.
|
|
196
|
+
if (( short_circuit )) && [[ "$exit_code" -eq 2 ]]; then
|
|
197
|
+
[[ -n "$last_stdout" ]] && printf '%s\n' "$last_stdout"
|
|
198
|
+
return 2
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
if [[ "$exit_code" -gt "$max_exit" ]]; then
|
|
202
|
+
max_exit="$exit_code"
|
|
203
|
+
fi
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
[[ -n "$last_stdout" ]] && printf '%s\n' "$last_stdout"
|
|
207
|
+
|
|
208
|
+
if (( dry_run )); then
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
return "$max_exit"
|
|
212
|
+
}
|