|
4 | 4 | # Purpose: |
5 | 5 | # One-time, best-effort hint nudging brand-new passthru users to run |
6 | 6 | # `/passthru:bootstrap` if they already have native permission rules in |
7 | | -# their `~/.claude/settings.json` that could be imported. Prints a |
8 | | -# single-line plain-text message on stdout, which Claude Code surfaces |
9 | | -# in the session header as "SessionStart:startup says: <text>". Then |
10 | | -# touches a marker so the hint does not fire again. |
| 7 | +# their `~/.claude/settings.json` that could be imported. Emits a JSON |
| 8 | +# envelope `{"systemMessage": "<text>"}` on stdout, which Claude Code |
| 9 | +# surfaces in the session view as "SessionStart:startup says: <text>". |
| 10 | +# Then touches a marker so the hint does not fire again. |
11 | 11 | # |
12 | 12 | # Contract: |
13 | 13 | # stdin - JSON envelope from Claude Code (SessionStart hook). We do |
14 | | -# not need any of its fields; we still drain stdin defensively |
15 | | -# so the writer does not hit SIGPIPE. |
16 | | -# stdout - a single line of plain text when the hint should fire, |
17 | | -# otherwise EMPTY. Per Claude Code docs, SessionStart stdout |
18 | | -# is appended to the session view as |
19 | | -# "SessionStart:startup says: <text>". Anything else (e.g. |
20 | | -# `{}`) would appear verbatim to the user. |
| 14 | +# not need any of its fields, and `exec < /dev/null` up front |
| 15 | +# ensures nothing downstream ever blocks on reading it. |
| 16 | +# stdout - a single-line JSON object `{"systemMessage":"<text>"}` when |
| 17 | +# the hint should fire, otherwise EMPTY. Per Claude Code |
| 18 | +# docs, SessionStart `systemMessage` is surfaced in the |
| 19 | +# session view as "SessionStart:startup says: <text>". Plain |
| 20 | +# text stdout does NOT surface - the JSON envelope is the |
| 21 | +# correct contract. |
21 | 22 | # exit - always 0. Any error fails open (empty stdout, exit 0). |
22 | 23 | # |
23 | 24 | # Gating: |
|
28 | 29 | # marker and exit silently. |
29 | 30 | # - If `~/.claude/settings.json` has no `.permissions.allow` entries, |
30 | 31 | # there is nothing to import - touch the marker and exit silently. |
31 | | -# - Otherwise: count the allow entries, emit the hint on stdout, touch |
32 | | -# the marker. |
| 32 | +# - Otherwise: count the allow entries, emit the hint as a JSON |
| 33 | +# `systemMessage` envelope on stdout, touch the marker. |
33 | 34 | # |
34 | 35 | # Paths honor PASSTHRU_USER_HOME and PASSTHRU_PROJECT_DIR so bats tests |
35 | 36 | # never touch the real ~/.claude. |
36 | 37 |
|
37 | 38 | set -euo pipefail |
38 | 39 |
|
| 40 | +# --------------------------------------------------------------------------- |
| 41 | +# Redirect stdin to /dev/null before sourcing common.sh or running any |
| 42 | +# command that might read from it. SessionStart never needs stdin contents, |
| 43 | +# and on macOS Claude Code can keep the pipe open across `claude --resume` |
| 44 | +# which would hang any cat/read call. This mirrors the pattern used by |
| 45 | +# memsearch's session-start.sh. |
| 46 | +# --------------------------------------------------------------------------- |
| 47 | +exec < /dev/null |
| 48 | + |
39 | 49 | # --------------------------------------------------------------------------- |
40 | 50 | # Locate and source common.sh (same pattern as pre/post handlers) |
41 | 51 | # --------------------------------------------------------------------------- |
|
58 | 68 | # --------------------------------------------------------------------------- |
59 | 69 | trap 'printf "[passthru] unexpected error in session-start.sh\n" >&2; exit 0' ERR |
60 | 70 |
|
61 | | -# --------------------------------------------------------------------------- |
62 | | -# Drain stdin defensively so the parent does not see SIGPIPE on its |
63 | | -# write end. We do not use the payload; SessionStart fields are not needed |
64 | | -# for this one-time hint. |
65 | | -# --------------------------------------------------------------------------- |
66 | | -if [ ! -t 0 ]; then |
67 | | - cat >/dev/null 2>&1 || true |
68 | | -fi |
69 | | - |
70 | 71 | USER_HOME="$(passthru_user_home)" |
71 | 72 | MARKER="${USER_HOME}/.claude/passthru.bootstrap-hint-shown" |
72 | 73 |
|
@@ -132,10 +133,17 @@ if [ "$COUNT" -eq 0 ]; then |
132 | 133 | fi |
133 | 134 |
|
134 | 135 | # --------------------------------------------------------------------------- |
135 | | -# 4. Emit the one-time hint on stdout and persist the marker. |
136 | | -# Single line keeps the session header readable. |
| 136 | +# 4. Emit the one-time hint on stdout as a JSON `systemMessage` envelope |
| 137 | +# and persist the marker. Claude Code surfaces the systemMessage in the |
| 138 | +# session view as "SessionStart:startup says: <text>". |
137 | 139 | # --------------------------------------------------------------------------- |
138 | | -printf 'passthru: detected %s importable permission rule(s) in ~/.claude/settings.json. Run /passthru:bootstrap to convert them. This tip only shows once.\n' "$COUNT" |
| 140 | +HINT_MSG="passthru: detected ${COUNT} importable permission rule(s) in ~/.claude/settings.json. Run /passthru:bootstrap to convert them. This tip only shows once." |
| 141 | + |
| 142 | +# jq -nc builds a compact JSON object with proper escaping. Fail-open: if |
| 143 | +# jq cannot produce output, we stay silent rather than emit broken JSON. |
| 144 | +if ENVELOPE="$(jq -nc --arg msg "$HINT_MSG" '{systemMessage:$msg}' 2>/dev/null)"; then |
| 145 | + printf '%s\n' "$ENVELOPE" |
| 146 | +fi |
139 | 147 |
|
140 | 148 | touch_marker |
141 | 149 | exit 0 |
0 commit comments