cc-session-recover 0.1.3 → 0.1.5

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.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # UserPromptSubmit hook.
4
+ # Injects an unconditional recovery order next to every prompt. Earlier
5
+ # softer wording ("if this is a multi-step task...") left the model
6
+ # discretion, and in long sessions it deferred forever. No judgment calls.
7
+ # Keep this output short: it is added to context on every single prompt.
8
+
9
+ set -u
10
+
11
+ printf 'Recovery check (do this before the request itself): if no recurring auto-continue schedule exists in this session, create one now — every 45 minutes, prompt: "Read .claude/auto-continue.md and follow it". Never create a duplicate. On multi-step tasks, update HANDOFF.md after each work step.\n'
12
+
13
+ exit 0
@@ -11,6 +11,16 @@
11
11
  ]
12
12
  }
13
13
  ],
14
+ "UserPromptSubmit": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/remind-on-prompt.sh"
20
+ }
21
+ ]
22
+ }
23
+ ],
14
24
  "StopFailure": [
15
25
  {
16
26
  "matcher": "rate_limit",
@@ -1,10 +1,12 @@
1
1
  Standing quota-recovery instructions for this project:
2
2
 
3
- If the user gives a multi-step coding task in this session:
3
+ At the start of any task in this session — coding, analysis, research, document work, audits, or skill-driven and automated runs:
4
4
 
5
- 1. Keep `HANDOFF.md` updated after every small safe step.
6
- 2. Create a recurring schedule every 45 minutes with this prompt: "Read .claude/auto-continue.md and follow it."
7
- 3. Cancel that schedule when the task is fully complete.
5
+ 1. Before doing the task itself, check whether this session already has a recurring auto-continue schedule.
6
+ 2. If none exists, create one now with Claude Code's scheduler (`CronCreate` or `/loop`). Do this even for short-looking requests; an unused schedule is harmless and you can cancel it when done.
7
+ 3. Use this scheduled prompt exactly: "Read .claude/auto-continue.md and follow it."
8
+ 4. Prefer a 45-minute recurring interval. If the scheduler cannot represent that cleanly, use a 30-minute recurring interval.
9
+ 5. Keep `HANDOFF.md` updated after every small safe step on multi-step tasks.
10
+ 6. Cancel the auto-continue schedule when the task is fully complete.
8
11
 
9
- Do not create the schedule for quick questions or single-step requests.
10
- Do not create a second schedule if one already exists.
12
+ Never create a second schedule if one already exists in this session.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 — 2026-06-13
4
+
5
+ - Add a `UserPromptSubmit` recovery reminder hook so the schedule instruction is repeated next to every prompt, not just at session start (it was getting buried in long sessions).
6
+ - Make the schedule rule unconditional and consistent across both injection points: always ensure one recurring auto-continue schedule exists, instead of leaving the model a "is this a long task?" judgment call that silently skipped audits and skill-driven runs. An explicit user instruction not to schedule still wins.
7
+ - Preserve existing `.claude/settings.local.json` on install by merging the recovery hooks in, rather than skipping when the file exists.
8
+ - Use the quoted `"$CLAUDE_PROJECT_DIR"` form in example hook commands (safe for paths with spaces).
9
+
10
+ ## 0.1.4 — 2026-06-12
11
+
12
+ - Expand standing quota-recovery instructions to cover long analysis, research, document, and automated workflow tasks, not only coding tasks.
13
+
3
14
  ## 0.1.3 — 2026-06-12
4
15
 
5
16
  - Install only the runtime `.claude` files and `HANDOFF.md` into target projects; package tooling now stays in the npm package.
package/README.md CHANGED
@@ -42,6 +42,6 @@ If quota dies mid-task, work resumes automatically after the reset.
42
42
 
43
43
  ## Docs
44
44
 
45
- - [Simple flow](docs/simple-flow.md) — how it works, told as a story (notebook, alarm, watchman).
46
- - [FAQ](docs/faq.md) — reliability, hook approval, what still needs a human.
47
- - [Full details](docs/claude-code-auto-resume.md) — closed-terminal watcher, precise reset-time resume, all limits.
45
+ - [Simple flow](https://github.com/softcane/cc-session-recover/blob/main/docs/simple-flow.md) — how it works, told as a story (notebook, alarm, watchman).
46
+ - [FAQ](https://github.com/softcane/cc-session-recover/blob/main/docs/faq.md) — reliability, hook approval, what still needs a human.
47
+ - [Full details](https://github.com/softcane/cc-session-recover/blob/main/docs/claude-code-auto-resume.md) — closed-terminal watcher, precise reset-time resume, all limits.
package/bin/cli.js CHANGED
@@ -23,6 +23,7 @@ const FILES = [
23
23
  '.claude/statusline-quota-cache.sh',
24
24
  '.claude/hooks/log-stop-failure.sh',
25
25
  '.claude/hooks/inject-standing-instructions.sh',
26
+ '.claude/hooks/remind-on-prompt.sh',
26
27
  ];
27
28
 
28
29
  const COPY_IF_MISSING = ['HANDOFF.md'];
@@ -38,6 +39,66 @@ const IGNORE_ENTRIES = [
38
39
  '.claude/quota-blocked.json',
39
40
  ];
40
41
  const IGNORE_HEADER = '# Claude Code session-recovery runtime state';
42
+ const RECOVERY_HOOK_SCRIPTS = [
43
+ 'inject-standing-instructions.sh',
44
+ 'remind-on-prompt.sh',
45
+ 'log-stop-failure.sh',
46
+ ];
47
+
48
+ function readJson(file) {
49
+ try {
50
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
51
+ } catch (err) {
52
+ throw new Error(`Could not parse ${file}: ${err.message}`);
53
+ }
54
+ }
55
+
56
+ function addMissingHookGroups(existingGroups, incomingGroups) {
57
+ const merged = Array.isArray(existingGroups) ? existingGroups.slice() : [];
58
+ for (const group of incomingGroups || []) {
59
+ const encoded = JSON.stringify(group);
60
+ if (!merged.some((existing) => JSON.stringify(existing) === encoded || hasSameRecoveryHook(existing, group))) {
61
+ merged.push(group);
62
+ }
63
+ }
64
+ return merged;
65
+ }
66
+
67
+ function recoveryHookScripts(group) {
68
+ const handlers = Array.isArray(group && group.hooks) ? group.hooks : [];
69
+ const commands = handlers
70
+ .filter((handler) => handler && handler.type === 'command' && typeof handler.command === 'string')
71
+ .map((handler) => handler.command);
72
+ return RECOVERY_HOOK_SCRIPTS.filter((script) => commands.some((command) => command.includes(script)));
73
+ }
74
+
75
+ function hasSameRecoveryHook(existingGroup, incomingGroup) {
76
+ const incomingScripts = recoveryHookScripts(incomingGroup);
77
+ if (!incomingScripts.length) return false;
78
+
79
+ const existingScripts = recoveryHookScripts(existingGroup);
80
+ return incomingScripts.some((script) => existingScripts.includes(script));
81
+ }
82
+
83
+ function mergeHookSettings(localPath, templatePath) {
84
+ if (!fs.existsSync(localPath)) {
85
+ fs.copyFileSync(templatePath, localPath);
86
+ return 'created';
87
+ }
88
+
89
+ const localSettings = readJson(localPath);
90
+ const templateSettings = readJson(templatePath);
91
+ const incomingHooks = templateSettings.hooks || {};
92
+ const mergedHooks = { ...(localSettings.hooks || {}) };
93
+
94
+ for (const [event, groups] of Object.entries(incomingHooks)) {
95
+ mergedHooks[event] = addMissingHookGroups(mergedHooks[event], groups);
96
+ }
97
+
98
+ localSettings.hooks = mergedHooks;
99
+ fs.writeFileSync(localPath, `${JSON.stringify(localSettings, null, 2)}\n`);
100
+ return 'merged';
101
+ }
41
102
 
42
103
  function usage() {
43
104
  console.error('Usage: cc-session-recover init [--no-hooks] [target-dir]');
@@ -109,11 +170,9 @@ function main() {
109
170
 
110
171
  if (enableHook) {
111
172
  const local = path.join(target, '.claude', 'settings.local.json');
112
- if (fs.existsSync(local)) {
113
- console.error('Skipped hook enablement: .claude/settings.local.json already exists.');
114
- console.error('Merge .claude/settings.example.json into it manually if wanted.');
115
- } else {
116
- fs.copyFileSync(path.join(TEMPLATE_ROOT, '.claude', 'settings.example.json'), local);
173
+ const result = mergeHookSettings(local, path.join(TEMPLATE_ROOT, '.claude', 'settings.example.json'));
174
+ if (result === 'merged') {
175
+ console.error('Merged hook settings into existing .claude/settings.local.json.');
117
176
  }
118
177
  }
119
178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-session-recover",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Quota-resume workflow for Claude Code: handoff notebook, heartbeat auto-continue, StopFailure hooks, and an unattended watcher that resumes after quota resets.",
5
5
  "scripts": {
6
6
  "check": "node --check bin/cli.js && bash -n scripts/*.sh .claude/hooks/*.sh .claude/statusline-quota-cache.sh && bash scripts/verify-claude-loop-workflow.sh",
@@ -27,12 +27,75 @@ TEMPLATE_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
27
27
  exit 1
28
28
  }
29
29
 
30
+ merge_hook_settings() {
31
+ local local_settings=$1
32
+ local template_settings=$2
33
+ local tmp
34
+
35
+ if [ ! -f "$local_settings" ]; then
36
+ cp "$template_settings" "$local_settings"
37
+ return
38
+ fi
39
+
40
+ if ! command -v jq >/dev/null 2>&1; then
41
+ printf 'Cannot merge hooks into existing .claude/settings.local.json because jq is not on PATH.\n' >&2
42
+ printf 'Install jq, then rerun this installer.\n' >&2
43
+ exit 1
44
+ fi
45
+
46
+ tmp=$(mktemp "${local_settings}.tmp.XXXXXX")
47
+ if jq -s '
48
+ def recovery_scripts: [
49
+ "inject-standing-instructions.sh",
50
+ "remind-on-prompt.sh",
51
+ "log-stop-failure.sh"
52
+ ];
53
+ def as_array: if type == "array" then . else [] end;
54
+ def as_object: if type == "object" then . else {} end;
55
+ def recovery_hook_scripts:
56
+ [(.hooks // [])[]? |
57
+ select(.type == "command" and (.command | type == "string")) |
58
+ .command as $command |
59
+ recovery_scripts[] |
60
+ select($command | contains(.))
61
+ ];
62
+ def same_recovery_hook($existing; $incoming):
63
+ (recovery_hook_scripts as $existing_scripts |
64
+ ($incoming | recovery_hook_scripts) as $incoming_scripts |
65
+ (($incoming_scripts | length) > 0) and
66
+ any($incoming_scripts[]; . as $script | any($existing_scripts[]; . == $script))
67
+ );
68
+ def add_missing($incoming):
69
+ as_array as $existing |
70
+ reduce (($incoming // []) | as_array)[] as $item ($existing;
71
+ if any(.[]; (. == $item) or same_recovery_hook(.; $item)) then . else . + [$item] end
72
+ );
73
+ def merge_hooks($incoming):
74
+ as_object as $base |
75
+ reduce (($incoming // {}) | as_object | keys_unsorted[]) as $event ($base;
76
+ .[$event] = (.[$event] | add_missing($incoming[$event]))
77
+ );
78
+
79
+ .[0] as $local |
80
+ .[1] as $template |
81
+ $local + {hooks: (($local.hooks // {}) | merge_hooks($template.hooks // {}))}
82
+ ' "$local_settings" "$template_settings" > "$tmp"; then
83
+ mv "$tmp" "$local_settings"
84
+ printf 'Merged hook settings into existing .claude/settings.local.json.\n' >&2
85
+ else
86
+ rm -f "$tmp"
87
+ printf 'Could not merge hooks into .claude/settings.local.json.\n' >&2
88
+ exit 1
89
+ fi
90
+ }
91
+
30
92
  mkdir -p "$TARGET/.claude/hooks"
31
93
 
32
94
  cp "$TEMPLATE_ROOT/.claude/auto-continue.md" "$TARGET/.claude/auto-continue.md"
33
95
  cp "$TEMPLATE_ROOT/.claude/standing-instructions.md" "$TARGET/.claude/standing-instructions.md"
34
96
  cp "$TEMPLATE_ROOT/.claude/statusline-quota-cache.sh" "$TARGET/.claude/statusline-quota-cache.sh"
35
97
  cp "$TEMPLATE_ROOT/.claude/hooks/inject-standing-instructions.sh" "$TARGET/.claude/hooks/inject-standing-instructions.sh"
98
+ cp "$TEMPLATE_ROOT/.claude/hooks/remind-on-prompt.sh" "$TARGET/.claude/hooks/remind-on-prompt.sh"
36
99
  cp "$TEMPLATE_ROOT/.claude/settings.example.json" "$TARGET/.claude/settings.example.json"
37
100
  cp "$TEMPLATE_ROOT/.claude/hooks/log-stop-failure.sh" "$TARGET/.claude/hooks/log-stop-failure.sh"
38
101
  if [ ! -f "$TARGET/HANDOFF.md" ]; then
@@ -41,6 +104,7 @@ fi
41
104
 
42
105
  chmod +x "$TARGET/.claude/hooks/log-stop-failure.sh"
43
106
  chmod +x "$TARGET/.claude/hooks/inject-standing-instructions.sh"
107
+ chmod +x "$TARGET/.claude/hooks/remind-on-prompt.sh"
44
108
  chmod +x "$TARGET/.claude/statusline-quota-cache.sh"
45
109
 
46
110
  # Keep runtime state out of the target's git history. HANDOFF.md must stay in
@@ -72,12 +136,7 @@ for entry in HANDOFF.md .claude/settings.local.json .claude/rate-limit-state.jso
72
136
  done
73
137
 
74
138
  if [ "$ENABLE_LOCAL_HOOK" -eq 1 ]; then
75
- if [ -f "$TARGET/.claude/settings.local.json" ]; then
76
- printf 'Skipped hook enablement because .claude/settings.local.json already exists.\n' >&2
77
- printf 'Merge .claude/settings.example.json into it manually if wanted.\n' >&2
78
- else
79
- cp "$TEMPLATE_ROOT/.claude/settings.example.json" "$TARGET/.claude/settings.local.json"
80
- fi
139
+ merge_hook_settings "$TARGET/.claude/settings.local.json" "$TEMPLATE_ROOT/.claude/settings.example.json"
81
140
  fi
82
141
 
83
142
  printf 'Installed Claude Code workflow into %s\n' "$TARGET"
@@ -44,6 +44,67 @@ bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" --enable-local-hook "$DUMM
44
44
  [ -f "$DUMMY/.claude/settings.local.json" ] || fail "hook settings not installed"
45
45
  [ ! -d "$DUMMY/scripts" ] || fail "install must not create a scripts folder in the target"
46
46
  [ ! -d "$DUMMY/docs" ] || fail "install must not create a docs folder in the target"
47
+ jq -e '.hooks.SessionStart and .hooks.UserPromptSubmit and .hooks.StopFailure' "$DUMMY/.claude/settings.local.json" >/dev/null \
48
+ || fail "installed local settings missing recovery hooks"
49
+ printf 'ok: installer activated recovery hooks in local settings\n'
50
+
51
+ step "Existing settings.local should keep settings and receive hooks"
52
+ MERGE_DUMMY="$WORK/merge-repo"
53
+ mkdir -p "$MERGE_DUMMY/.claude"
54
+ printf '{"permissions":{"allow":["Bash(npm test *)"]},"hooks":{"PostToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"true"}]}]}}\n' \
55
+ > "$MERGE_DUMMY/.claude/settings.local.json"
56
+ bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" "$MERGE_DUMMY" >/dev/null
57
+ jq -e '
58
+ (.permissions.allow[0] == "Bash(npm test *)") and
59
+ (.hooks.PostToolUse[0].matcher == "Write") and
60
+ (.hooks.SessionStart | length > 0) and
61
+ (.hooks.UserPromptSubmit | length > 0) and
62
+ (.hooks.StopFailure | length > 0)
63
+ ' "$MERGE_DUMMY/.claude/settings.local.json" >/dev/null || fail "bash installer did not merge hooks into existing settings"
64
+ bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" "$MERGE_DUMMY" >/dev/null
65
+ DUPES=$(jq '[.hooks.SessionStart[], .hooks.UserPromptSubmit[], .hooks.StopFailure[]] | length' "$MERGE_DUMMY/.claude/settings.local.json")
66
+ [ "$DUPES" -eq 3 ] || fail "bash installer duplicated recovery hooks on rerun"
67
+ printf 'ok: bash installer preserved existing settings and merged hooks once\n'
68
+
69
+ step "Older shell-form recovery hooks should not be duplicated"
70
+ OLD_DUMMY="$WORK/old-hook-repo"
71
+ mkdir -p "$OLD_DUMMY/.claude"
72
+ jq -n '{
73
+ hooks: {
74
+ SessionStart: [
75
+ {
76
+ hooks: [
77
+ {
78
+ type: "command",
79
+ command: "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-standing-instructions.sh"
80
+ }
81
+ ]
82
+ }
83
+ ]
84
+ }
85
+ }' > "$OLD_DUMMY/.claude/settings.local.json"
86
+ bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" "$OLD_DUMMY" >/dev/null
87
+ SESSION_START_COUNT=$(jq '.hooks.SessionStart | length' "$OLD_DUMMY/.claude/settings.local.json")
88
+ [ "$SESSION_START_COUNT" -eq 1 ] || fail "installer duplicated old SessionStart recovery hook"
89
+ jq -e '(.hooks.UserPromptSubmit | length > 0) and (.hooks.StopFailure | length > 0)' \
90
+ "$OLD_DUMMY/.claude/settings.local.json" >/dev/null || fail "installer failed to add missing hooks next to old hook"
91
+ printf 'ok: installer recognized old recovery hook commands and added only missing hooks\n'
92
+
93
+ step "Node installer should also merge existing settings.local"
94
+ NODE_DUMMY="$WORK/node-merge-repo"
95
+ mkdir -p "$NODE_DUMMY/.claude"
96
+ printf '{"permissions":{"allow":["Bash(npm test *)"]}}\n' > "$NODE_DUMMY/.claude/settings.local.json"
97
+ node "$TEMPLATE_ROOT/bin/cli.js" init "$NODE_DUMMY" >/dev/null
98
+ jq -e '
99
+ (.permissions.allow[0] == "Bash(npm test *)") and
100
+ (.hooks.SessionStart | length > 0) and
101
+ (.hooks.UserPromptSubmit | length > 0) and
102
+ (.hooks.StopFailure | length > 0)
103
+ ' "$NODE_DUMMY/.claude/settings.local.json" >/dev/null || fail "node installer did not merge hooks into existing settings"
104
+ node "$TEMPLATE_ROOT/bin/cli.js" init "$NODE_DUMMY" >/dev/null
105
+ DUPES=$(jq '[.hooks.SessionStart[], .hooks.UserPromptSubmit[], .hooks.StopFailure[]] | length' "$NODE_DUMMY/.claude/settings.local.json")
106
+ [ "$DUPES" -eq 3 ] || fail "node installer duplicated recovery hooks on rerun"
107
+ printf 'ok: node installer preserved existing settings and merged hooks once\n'
47
108
 
48
109
  step "Fake SessionStart: standing instructions should be injected"
49
110
  INJECTED=$(printf '{"session_id":"fake-session-123","hook_event_name":"SessionStart","source":"startup"}' \
@@ -60,13 +121,15 @@ printf '{"workspace":{"project_dir":"%s"},"model":{"display_name":"Test"},"rate_
60
121
  printf 'ok: status line wrapper cached the reset time\n'
61
122
 
62
123
  step "Fake quota stop: StopFailure(rate_limit) should log and write the marker"
63
- printf '{"session_id":"fake-session-123","hook_event_name":"StopFailure","error_type":"rate_limit"}' \
124
+ printf '{"session_id":"fake-session-123","hook_event_name":"StopFailure","error":"rate_limit","last_assistant_message":"API Error: Rate limit reached"}' \
64
125
  | CLAUDE_PROJECT_DIR="$DUMMY" bash "$DUMMY/.claude/hooks/log-stop-failure.sh"
65
126
  [ -f "$DUMMY/.claude/stop-failure-events.jsonl" ] || fail "missing stop-failure log"
66
127
  [ -f "$DUMMY/.claude/quota-blocked.json" ] || fail "missing quota-blocked marker"
67
128
  grep -Fq "hit a rate limit" "$DUMMY/HANDOFF.md" || fail "missing handoff note"
68
129
  SESSION=$(jq -r '.hook_input.session_id // empty' "$DUMMY/.claude/quota-blocked.json")
69
130
  [ "$SESSION" = "fake-session-123" ] || fail "marker has wrong session_id: $SESSION"
131
+ ERROR=$(jq -r '.hook_input.error // empty' "$DUMMY/.claude/quota-blocked.json")
132
+ [ "$ERROR" = "rate_limit" ] || fail "marker has wrong error field: $ERROR"
70
133
  RESETS=$(jq -r '.rate_limit_state.five_hour_resets_at // empty' "$DUMMY/.claude/quota-blocked.json")
71
134
  [ -n "$RESETS" ] || fail "marker missing cached reset time"
72
135
  printf 'ok: hook wrote log, handoff note, and marker with session_id and reset time\n'
@@ -25,6 +25,7 @@ require_file "HANDOFF.md"
25
25
  require_file ".claude/settings.example.json"
26
26
  require_file ".claude/hooks/log-stop-failure.sh"
27
27
  require_file ".claude/hooks/inject-standing-instructions.sh"
28
+ require_file ".claude/hooks/remind-on-prompt.sh"
28
29
  require_file ".claude/standing-instructions.md"
29
30
  require_file "scripts/test-fake-quota-flow.sh"
30
31
  require_file "README.md"
@@ -62,6 +63,10 @@ require_text ".claude/settings.example.json" "SessionStart"
62
63
  require_text ".claude/settings.example.json" "inject-standing-instructions.sh"
63
64
  require_text ".claude/standing-instructions.md" "auto-continue.md"
64
65
  require_text ".claude/standing-instructions.md" "HANDOFF.md"
66
+ require_text ".claude/settings.example.json" "UserPromptSubmit"
67
+ require_text ".claude/settings.example.json" "remind-on-prompt.sh"
68
+ require_text ".claude/hooks/remind-on-prompt.sh" "HANDOFF.md"
69
+ require_text ".claude/hooks/remind-on-prompt.sh" "auto-continue.md"
65
70
  require_text ".claude/settings.example.json" "StopFailure"
66
71
  require_text ".claude/settings.example.json" "rate_limit"
67
72
  require_text ".claude/settings.example.json" "log-stop-failure.sh"
@@ -78,6 +83,7 @@ require_text "scripts/quota-watcher.sh" "session_id"
78
83
  [ -x "$ROOT/scripts/install-into-project.sh" ] || fail "scripts/install-into-project.sh must be executable"
79
84
  [ -x "$ROOT/scripts/quota-watcher.sh" ] || fail "scripts/quota-watcher.sh must be executable"
80
85
  [ -x "$ROOT/.claude/hooks/inject-standing-instructions.sh" ] || fail ".claude/hooks/inject-standing-instructions.sh must be executable"
86
+ [ -x "$ROOT/.claude/hooks/remind-on-prompt.sh" ] || fail ".claude/hooks/remind-on-prompt.sh must be executable"
81
87
  [ -x "$ROOT/scripts/test-fake-quota-flow.sh" ] || fail "scripts/test-fake-quota-flow.sh must be executable"
82
88
 
83
89
  for forbidden in "claude -p" "tmux" "screen" "expect" "TIOCSTI"; do