@penguinharness/use-claude-code 0.2.9

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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/icon.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="3" y="8" width="14" height="12" rx="2" />
3
+ <path d="M6.3 12.2l2.7 2.3-2.7 2.3" />
4
+ <path d="M11 17.2h3.2" />
5
+ <path d="M17.5 5.2a3 3 0 0 1 3 3" />
6
+ <path d="M17.5 2.7a5.5 5.5 0 0 1 5.5 5.5" />
7
+ </svg>
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@penguinharness/use-claude-code",
3
+ "version": "0.2.9",
4
+ "description": "Run Claude Code on a remote host over SSH — a persistent expect-driven login session, headless claude -p with the stdin fix, the interactive TUI inside a remote tmux driven by send-keys/capture-pane (one keystroke at a time, capture-verified; relayed user messages go through verbatim), and multi-turn continuity via --session-id/--resume or stream-json; hosts and credentials are placeholders resolved at runtime from the user or the vault, never hardcoded.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Prism-Shadow/penguin-harness.git",
9
+ "directory": "plugins/use-claude-code"
10
+ },
11
+ "files": [
12
+ "plugin.json",
13
+ "icon.svg",
14
+ "skills",
15
+ "hooks",
16
+ "LICENSE"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ }
21
+ }
package/plugin.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "Run Claude Code on a remote host over SSH — a persistent expect-driven login session, headless claude -p with the stdin fix, the interactive TUI inside a remote tmux driven by send-keys/capture-pane (one keystroke at a time, capture-verified; relayed user messages go through verbatim), and multi-turn continuity via --session-id/--resume or stream-json; hosts and credentials are placeholders resolved at runtime from the user or the vault, never hardcoded.",
3
+ "short_description": "Run Claude Code on remote hosts over SSH.",
4
+ "short_description_zh": "在远程主机上通过 SSH 运行 Claude Code(持久会话 / headless / tmux 交互 / 多轮续接)。",
5
+ "version": "2026-08-18.1",
6
+ "category": "software-development",
7
+ "preinstall": false
8
+ }
@@ -0,0 +1,212 @@
1
+ ---
2
+ name: remote-claude-code
3
+ description: Run Claude Code on a remote host over SSH — a persistent expect-driven login session, headless claude -p with the stdin fix, the interactive TUI inside a remote tmux driven by send-keys/capture-pane (one keystroke at a time, capture-verified; relayed user messages go through verbatim), and multi-turn continuity via --session-id/--resume or stream-json; hosts and credentials are placeholders resolved at runtime from the user or the vault, never hardcoded.
4
+ ---
5
+
6
+ # Remote Claude Code
7
+
8
+ Drive Claude Code on a remote Linux host over SSH. Three modes, in increasing interactivity:
9
+
10
+ 1. **Persistent SSH session** — one long-lived expect-driven connection you keep feeding commands across turns.
11
+ 2. **Headless (`claude -p`)** — one-shot or scripted calls, with the stdin fix that naive invocations need.
12
+ 3. **Interactive TUI** — the real Claude Code UI inside a remote `tmux` session, driven with `tmux send-keys` / `tmux capture-pane`. tmux is the way to do interactive use.
13
+
14
+ Reach for this skill when the user wants to run or drive Claude Code on a server, keep an SSH connection open across turns, or hold a continuous conversation with Claude Code on a remote box. In a relayed conversation you are a pure message pipe — the user's words go to Claude Code verbatim (section 4).
15
+
16
+ ## Before you start
17
+
18
+ If the user's message only invokes this skill without a concrete task, ask what they want — at minimum the remote host, the SSH user, and what Claude Code should do there.
19
+
20
+ Credential rules — non-negotiable:
21
+
22
+ - This document contains **no real credentials**: `<ssh-user>`, `<remote-host>`, `<target-user>`, `<sess>` are placeholders you substitute at runtime from what the user provides or from this agent's **key vault** (suggested keys: `REMOTE_SSH_HOST`, `REMOTE_SSH_USER`, `REMOTE_SSH_PASSWORD`). Vault values reach your shell environment on the next task — check with `[ -n "$REMOTE_SSH_PASSWORD" ] && echo ok || echo missing`, and if missing ask the user to add the keys to the key vault (gear icon on the agent card → settings → key vault tab) or to provide the values in chat.
23
+ - **Never hardcode a password** into scripts left on disk, deliverables, or your final answer. Have expect read it from the environment (`$env(...)`); if the user pasted it in chat, export it only into the running process's environment, scrub it from logs and replies, and delete any temp file that embeds it as soon as the session is done.
24
+ - Prefer SSH keys when they are already set up — then no password tooling is needed at all.
25
+ - Back up remote config before modifying it, and never copy remote credential stores (`~/.claude/.credentials.json`, OAuth fields of `~/.claude.json`, private keys) anywhere.
26
+
27
+ ## 1. Persistent SSH session (expect)
28
+
29
+ One long-lived connection with password auto-login and an optional user switch, held open by `interact`. `expect` ships with macOS and is a one-command install on Linux; `sshpass` is usually absent, so expect is the password-interactive tool of choice.
30
+
31
+ The full expect template plus its operating notes live in [`reference/persistent-session.md`](reference/persistent-session.md) — read that file when setting up this mode. In short: write the template to your scratchpad, fill the placeholders at runtime (the password comes from `$env(REMOTE_SSH_PASSWORD)`, never hardcoded), start it with `exec_command`, and drive the held-open connection through its `process_id` with `input_command`. Reuse this one session for most remote commands rather than opening a new connection each time.
32
+
33
+ ## 2. Headless Claude Code (`claude -p`) — the stdin gotcha
34
+
35
+ `claude -p "<prompt>"` hangs forever when its stdin is an open pipe that never closes — which is exactly what agent harnesses and the expect session above provide. The process idles with no output and no network connections. Fix: redirect stdin from `/dev/null`:
36
+
37
+ ```bash
38
+ claude -p "Reply with exactly: all good" < /dev/null
39
+ ```
40
+
41
+ Same for `--resume`, `--session-id`, `--output-format stream-json`, and the rest. Exception: when input is _meant_ to come from stdin (`--input-format stream-json`), feed it through a pipe that closes (`echo '<json>' | claude ...`) and do **not** add `< /dev/null` — that would override the pipe.
42
+
43
+ Symptom checklist: process running + no output + no TCP connections → stdin starvation; add `< /dev/null`.
44
+
45
+ ## 3. Interactive TUI — tmux is the way
46
+
47
+ Launching `claude` interactively inside the expect/pipe session renders the welcome screen, but pipe input is treated as a **paste**: text lands in the input box and Enter (`\r`) is inserted literally, so the message is never submitted (the remote transcript `~/.claude/projects/<dir>/*.jsonl` gains no user entry). Run Claude Code inside a remote `tmux` session instead and drive it with `tmux send-keys`, which delivers discrete keypresses:
48
+
49
+ ```bash
50
+ # start detached, then read the screen
51
+ TERM=xterm-256color tmux new-session -d -s <sess> "claude"
52
+ sleep 8
53
+ tmux capture-pane -t <sess> -p | tail -25
54
+
55
+ # converse: type the message, confirm it landed, submit — text and Enter as separate calls (§3.3)
56
+ tmux send-keys -t <sess> -l "Introduce yourself in one sentence"
57
+ tmux capture-pane -t <sess> -p | tail -5 # exactly your message on the input line?
58
+ tmux send-keys -t <sess> Enter
59
+ sleep 20
60
+ tmux capture-pane -t <sess> -p -S -200 # scrollback: a long reply overflows the visible pane
61
+ ```
62
+
63
+ - Workspace trust dialog: it shows on the first launch in each project directory (`-p` mode skips it; `--dangerously-skip-permissions` does **not**). Answer it once via `tmux send-keys -t <sess> Enter` and capture to confirm it closed, or pre-accept it by setting `hasTrustDialogAccepted: true` for that project path in the remote `~/.claude.json` — back the file up first (`cp ~/.claude.json ~/.claude.json.bak-$(date +%s)`) and rewrite it with a JSON-aware tool (python), not sed.
64
+ - Tool-permission prompts, and any other question Claude Code puts to **the user** (clarifying questions, plan approvals): the decision is the user's, not yours. Report the prompt and its options, wait for the user's answer, then send that key (one key, capture-verified, §3.3). Never approve a tool call on the user's behalf — an approved command runs on their host. `--dangerously-skip-permissions` is the one standing approval you may act on, and only when the user explicitly asked for unattended runs on that host.
65
+ - `TERM=xterm-256color` must be visible to the tmux **server** (prefix the `tmux new-session` call that starts it), or the TUI renders garbled and colorless.
66
+ - Continuity is real: within one tmux session, follow-ups remember earlier turns ("what was my first question?" gets the right answer).
67
+ - The user can join from their own terminal at any time: `ssh <ssh-user>@<remote-host>` → `su - <target-user>` → `tmux attach -t <sess>`.
68
+ - The session can die under you (host rebooted, `tmux kill-server`, someone closed it). A `capture-pane` that exits non-zero with `can't find pane: <sess>` or `no server running on ...` means exactly that — not a slow TUI, so don't retry keys into it. Confirm with `tmux has-session -t <sess>` (exit 0 alive, 1 gone), tell the user the session is gone, and rebuild it with `claude --continue` in the same working directory to pick the conversation back up. Never start a bare `claude` and keep relaying as if the earlier context were still there.
69
+
70
+ ### 3.1 Waiting out a long turn — don't hold SSH open
71
+
72
+ A Claude Code turn can run for ten-plus minutes. Instead of keeping an SSH connection open to poll, place a **detached watcher** on the remote (`setsid nohup`, so it outlives the launching SSH session): it polls `tmux capture-pane` every 10s, and once the turn has been idle for 3 consecutive checks — the footer no longer shows `esc to interrupt` — it writes the final screen plus a `DONE` marker (or `TIMEOUT` past a cap). A blocking SSH loop then waits for the marker and returns the final screen, so the tool call effectively hangs until the turn is done. The full watcher script and the wait loop are in [`reference/completion-watcher.md`](reference/completion-watcher.md) — read it when you need this.
73
+
74
+ ### 3.2 Delivering a message — and why input-line text may be a suggestion
75
+
76
+ When an idle Claude Code TUI shows text on the input line (after `❯`) — above all right after a turn finishes — that is usually a **Claude-generated suggested next message**, not text the user typed and left pending, and a `capture-pane` dump cannot reliably tell the two apart. So never treat post-run input-line text as pending user input: don't submit it, don't report it to the user as their unsent draft, and don't try to clear or edit it. `tmux send-keys` only ever adds new text — it does not edit or "complete" that suggestion, and Enter submits what _you_ sent, not the suggestion. When the next message is due, just send it — new text makes the suggestion disappear on its own.
77
+
78
+ **A single plain line** goes through `send-keys -l` (literal mode, so punctuation and words like `Enter` aren't parsed as key names). Text and Enter are always separate calls, with a capture in between:
79
+
80
+ ```bash
81
+ tmux send-keys -t <sess> -l "Your full message, punctuation and all"
82
+ tmux capture-pane -t <sess> -p | tail -5 # exactly your message on the input line?
83
+ tmux send-keys -t <sess> Enter
84
+ ```
85
+
86
+ **Anything else — multi-line, pasted, long, or punctuation-heavy — goes through the paste buffer**, because `send-keys -l` corrupts exactly the messages a relay must not corrupt (both verified on tmux 3.3a):
87
+
88
+ - a newline inside the literal text is delivered as **Return**, so a two-line argument submits its first line on its own and leaves the second as a fresh draft — one user message becomes two Claude Code turns;
89
+ - a `;` that ends the argument is swallowed by tmux's own command-sequence parser: `-l "ends with semi;"` arrives as `ends with semi`. Characters vanish and nothing reports an error.
90
+
91
+ Load the text from a **quoted** heredoc (so neither the shell nor tmux re-parses `$`, backticks, quotes or backslashes) and paste it as one block. This uses the paste behavior §3 warns about on purpose: a bracketed paste lands in the input box as a **draft** and does not submit, so the separate `Enter` keypress stays the thing that sends it.
92
+
93
+ ```bash
94
+ tmux load-buffer - <<'MSG'
95
+ The user's message, exactly as written —
96
+ newlines, "quotes", $signs and semicolons; all intact.
97
+ MSG
98
+ tmux paste-buffer -d -p -t <sess> # -p bracketed paste (stays a draft), -d drops the buffer after
99
+ tmux capture-pane -t <sess> -p | tail -8
100
+ tmux send-keys -t <sess> Enter
101
+ ```
102
+
103
+ The landed check is **exact, not approximate**: the input line must show your message and nothing else. A large paste may collapse to a `[Pasted text #N +K lines]` placeholder instead of the text — that still counts as landed; check the line count, not the words. Two failures to watch for, both before you press Enter:
104
+
105
+ - **Merged** — your text sits next to something else on the line. Don't submit: clear the line (`tmux send-keys -t <sess> C-u`), send again, and if it still merges, tell the user rather than submitting a message they didn't write.
106
+ - **Already submitted** — the turn started on its own, without your Enter. That pane isn't honoring bracketed paste, so the message went in split across lines. Say so, and deliver the rest one `-l` line at a time rather than pasting again.
107
+
108
+ Once the input line is right, send Enter, then `capture-pane` again to confirm the turn started.
109
+
110
+ ### 3.3 One keystroke at a time — capture between keys
111
+
112
+ The TUI processes keys asynchronously, so a batched sequence races it: `tmux send-keys -t <sess> Up s` delivers `s` before the menu has processed `Up`, and `s` acts on the **previous** selection. Two back-to-back `send-keys` calls with no check in between race the same way.
113
+
114
+ In every menu, picker, or dialog (`/model`, permission prompts, trust dialog), send **one key per `send-keys` call** and verify between keys: send → `capture-pane` → confirm the expected change (highlight moved, dialog opened or closed, value updated) → only then send the next key.
115
+
116
+ Know where you are going before you start: capture the picker once, read which row is highlighted now and which row you want, count the moves, and step that many times — never press `Up` and hope.
117
+
118
+ ```bash
119
+ tmux send-keys -t <sess> Up
120
+ tmux capture-pane -t <sess> -p | tail -15 # highlight moved to the intended entry?
121
+ tmux send-keys -t <sess> Enter
122
+ tmux capture-pane -t <sess> -p | tail -15 # menu closed, new value shown?
123
+ ```
124
+
125
+ If the capture doesn't show the change yet, wait a second and capture again — never fire the next key on faith. Re-capture up to three times (a few seconds in total), then act on what the screen actually shows instead of sending more keys blindly:
126
+
127
+ - **Nothing changed** — the key was swallowed: re-send that same key once, and verify again.
128
+ - **The wrong thing changed** — highlight moved the other way, a different dialog opened, a value flipped that you didn't touch: correct it with the opposite key (`Down` for a stray `Up`, `Escape` for a dialog you didn't want) and verify again. Never "fix" a wrong state by pressing on toward the goal.
129
+ - **Still not where you want it after two corrections** — stop. Show the user the captured screen and ask. A menu whose state you can't read is not something to improvise more keystrokes into: a wrong key in `/model` changes their model, and a wrong key in a permission prompt runs a command on their host.
130
+
131
+ Message text is the one exception to one-key-at-a-time: send the whole message in a single call (`-l` for one plain line, `load-buffer`/`paste-buffer` otherwise), confirm it sits on the input line, then Enter as its own keypress (§3.2).
132
+
133
+ ### 3.4 Model and thinking level are two separate settings
134
+
135
+ A switch request that names a model **plus a level word** — "switch to fable5 max", "use opus high", "切换到 fable5 max" — means **two** settings: switch the **model** to the named one (Fable 5) **and** the **thinking level** to the named level (max). Never read the pair as one unknown model name, and never forward it as chat text for Claude Code to answer — it is session control you execute in the TUI (section 4).
136
+
137
+ The rule is about the shape of the request, not about those three examples. It fires on **any** message naming a model, a thinking level, or both, in any language — "换成 opus 高", "改成 max", "set thinking to high", "用 sonnet":
138
+
139
+ - **Level words** are `max`, `high`, `medium`/`med`, `low`, `none`/`off` and their Chinese equivalents (`最高`/`最大`, `高`, `中`/`中等`, `低`, `关`/`不思考`). A level word sitting next to a model name is always the thinking level — never part of the model's name.
140
+ - **Model + level** ("fable5 max") → set both, model first.
141
+ - **Level only** ("改成 max", "set thinking to high") → change the thinking level, leave the model alone.
142
+ - **Model only** ("switch to opus", "用 sonnet") → change the model, leave the level alone.
143
+ - Names arrive shortened, unspaced, or in the wrong case (`fable5` → Fable 5, `opus` → the Opus entry). Match them against the rows the picker actually shows; if nothing matches, say what the picker offers and ask — never pick the nearest-looking row.
144
+
145
+ Use Claude Code's own controls: type `/model` (`-l`, then Enter as its own key) and walk the picker one keystroke at a time per §3.3; when the build exposes the thinking level as its own entry or toggle rather than a per-model variant, set it in a second step the same way. Finish with a capture that shows **both** the new model and the new thinking level before telling the user the switch is done — for a level-only or model-only request, that the requested one changed **and** the other did not.
146
+
147
+ ## 4. Relaying a conversation — the user's words go through verbatim
148
+
149
+ Once the session is up and the user is talking to Claude Code **through** you, you are a message pipe — nothing more:
150
+
151
+ - Forward every user message to Claude Code **verbatim**: the exact wording, unchanged — no answering it yourself, no rephrasing or translating, no summarizing, no doing any part of the task locally. Even when you know the answer or the task looks trivial: the user is talking to Claude Code, not to you.
152
+ - Deliver the message (§3.2), wait out the turn (§3.1), then report what Claude Code said — faithfully, without your own analysis, edits, or additions. Read the reply with scrollback (`capture-pane -p -S -200`), not `| tail -30`: a long answer runs off the visible pane and a tail hands the user a fragment presented as the whole reply.
153
+ - **Questions travel back, not to you.** When Claude Code asks something — a permission prompt, a clarifying question, a plan to approve — carry it to the user with its options and send the answer they give (§3.3). You never answer on their behalf; that is their conversation.
154
+ - **A message that arrives mid-turn waits.** If the footer still shows `esc to interrupt`, don't type into the running turn: hold the message, wait the turn out (§3.1), deliver it then, and tell the user it is queued behind the current run. If they clearly want to stop or redirect now ("stop", "停", "别做了"), send `Escape` as its own key, capture to confirm the working footer is gone, then deliver. Escape twice in a row is not a stronger interrupt — it opens Claude Code's rewind-to-an-earlier-message UI, so send one and check.
155
+ - The only requests you act on yourself are **session control**: model / thinking-level switches (§3.4), interrupting a stuck turn (Escape), attaching, detaching, or closing the session, and connection repair. Execute those against the TUI; everything else goes into the conversation verbatim.
156
+ - **Split by subject, not by phrasing.** Questions about the **session** — is it still running, what's on the screen, is the connection alive, did the turn finish — you answer yourself from a capture. Anything about the **work** goes through unchanged, including questions you could answer. A message that does both ("is it still going? when it's done have it add the tests too") goes to Claude Code in full, and you answer the session half locally.
157
+ - When in doubt whether a message is task or control, relay it verbatim — a mis-relayed control request is easy to recover; work silently done in Claude Code's place is not.
158
+
159
+ ## 5. Continuous (multi-turn) conversation
160
+
161
+ Three verified ways to keep context across calls:
162
+
163
+ **a) Same session across headless invocations (simplest):**
164
+
165
+ ```bash
166
+ SID=$(uuidgen)
167
+ claude -p "Remember: my name is Alex." --session-id "$SID" < /dev/null
168
+ claude -p "What is my name?" --resume "$SID" < /dev/null # answers: Alex
169
+ ```
170
+
171
+ **b) stream-json real-time protocol (programmatic):**
172
+
173
+ ```bash
174
+ echo '{"type":"user","message":{"role":"user","content":"Hello"}}' \
175
+ | claude -p --verbose --input-format stream-json --output-format stream-json
176
+ ```
177
+
178
+ Returns `system/init` (carrying the `session_id`), `assistant`, and `result` events; reuse the `session_id` to continue. `--output-format stream-json` requires `--verbose`.
179
+
180
+ **c) The interactive tmux session (section 3)** — the true REPL-style continuous conversation.
181
+
182
+ ## 6. Troubleshooting
183
+
184
+ | Symptom | Cause / fix |
185
+ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
186
+ | `claude -p` runs forever, no output, no TCP connections | stdin pipe never closes → add `< /dev/null` |
187
+ | TUI renders once then ignores keys; Enter shows up literally | pipe input treated as a paste → drive it with tmux `send-keys` |
188
+ | Trust dialog on every launch | `hasTrustDialogAccepted` unset for that project dir → set it to true (back up `~/.claude.json` first) |
189
+ | TUI garbled / no colors | `TERM=dumb` → set `xterm-256color` at expect spawn or tmux launch |
190
+ | expect stuck producing no output | a prompt regex never matched → replace it with `sleep` + fixed sends |
191
+ | `Connection timed out during banner exchange` | busy server / transient → wait a few seconds and retry; keep one persistent session instead of hammering new ones |
192
+ | New SSH connections crawl while old ones work | connection pressure → route commands through the persistent session |
193
+ | Input line shows text you didn't type | a Claude Code suggestion, common right after a turn ends — not pending user input (see §3.2) → don't submit or edit it; `send-keys -l "<message>"` then `Enter` |
194
+ | Menu key acts on the wrong item (`Up` and `s` sent together) | keys batched faster than the TUI processes them → one key per `send-keys` call, `capture-pane` between keys (§3.3) |
195
+ | User asks for "fable5 max" / "opus high" / "改成 max" | model and/or thinking level — session control in the TUI (§3.4), not one unknown model, not a chat message to relay |
196
+ | Relayed message arrived split in two, or lost its last character | `send-keys -l` delivered an embedded newline as Return, or tmux ate a trailing `;` → deliver anything non-trivial with `load-buffer` + `paste-buffer -d -p` (§3.2) |
197
+ | `capture-pane` fails: `can't find pane` / `no server running` | the tmux session or server is gone → confirm with `tmux has-session -t <sess>`, tell the user, rebuild with `claude --continue`; never keep relaying into a fresh, context-less session (§3) |
198
+ | Menu key does nothing, or moves the wrong way | key swallowed or landed on the wrong row → re-send once / correct with the opposite key, verify; after two failed corrections stop and show the user the screen (§3.3) |
199
+ | User sends a new message while a turn is running | don't type into a live turn → hold it until the turn ends (§3.1), or `Escape` once (never twice) if they want to stop now (§4) |
200
+ | Want to wait out a long turn without holding SSH open | run a detached watcher on the remote (see §3.1) that polls `capture-pane` until idle and writes a `DONE`/`TIMEOUT` marker, then block on an SSH loop until it appears |
201
+
202
+ ## 7. Verification checklist
203
+
204
+ - Persistent session: the probe returns `whoami`/`hostname`/`pwd`, and the prompt comes back after each command.
205
+ - Headless: `claude -p "..." < /dev/null` prints the answer and exits.
206
+ - Interactive: `capture-pane` shows your message on the input line, then the response, then the prompt again.
207
+ - Relay: what reached Claude Code's input line is word-for-word what the user sent — nothing merged in from a suggestion, no line split off, no character dropped — and the reply you report back is Claude Code's, read with scrollback so it isn't a truncated tail.
208
+ - Decisions: every permission prompt and question Claude Code raised was answered by the user, not by you.
209
+ - Menus: every keystroke was its own `send-keys` call, and a capture confirmed the expected state before the next key.
210
+ - Switches: after a model and/or thinking-level request, one final capture shows the requested value(s) changed and the untouched one unchanged.
211
+ - Continuity: a follow-up that requires memory answers correctly (or the remote `~/.claude/projects/**/*.jsonl` shows the user entry).
212
+ - Sanitization: no real host, user, or password appears in temp scripts left on disk or in your final answer — real values only ever come from the user or the vault at runtime.
@@ -0,0 +1,69 @@
1
+ # Waiting out a long turn — remote completion watcher
2
+
3
+ A single Claude Code turn can run for ten-plus minutes. Rather than hold an SSH connection open
4
+ polling `tmux capture-pane`, place a **detached watcher** on the remote (`setsid nohup`, so it
5
+ outlives the SSH session that launched it). It polls the pane every 10s and, once the turn has been
6
+ idle for 3 consecutive checks — the footer no longer shows the working indicator `esc to interrupt`
7
+ (rendered as e.g. `✳ Cultivating… (esc to interrupt)` while a turn runs) — writes the final screen
8
+ to a file with a `DONE` marker; past a hard cap it writes `TIMEOUT` instead. A blocking SSH loop
9
+ then waits for the marker and returns the final screen, so the tool call "hangs until done".
10
+
11
+ Replace `<sess>` with your tmux session name. `esc to interrupt` is the stable part of the working
12
+ footer; the gerunds (`Cultivating`, `Improvising`, …) are flavor text that varies by version, so
13
+ key detection on `esc to interrupt` and treat the rest as backup.
14
+
15
+ ## 1. Install and start the watcher (detached)
16
+
17
+ ```bash
18
+ ssh <ssh-user>@<remote-host> 'cat > ~/cc-watch.sh <<'"'"'EOF'"'"'
19
+ #!/usr/bin/env bash
20
+ LOG=~/cc-watch.log; OUT=~/cc-final.txt
21
+ rm -f "$LOG" "$OUT"
22
+ busy_seen=0; idle_count=0; max_wait=7200; start=$(date +%s)
23
+ echo "WATCHER START $(date)" >> "$LOG"
24
+ while :; do
25
+ now=$(date +%s)
26
+ if (( now - start > max_wait )); then
27
+ tmux capture-pane -t <sess> -p > "$OUT"; echo "TIMEOUT $(date)" >> "$LOG"; exit 0
28
+ fi
29
+ live=$(tmux capture-pane -t <sess> -p 2>/dev/null | tail -30)
30
+ if printf "%s\n" "$live" | grep -qE "esc to interrupt|Cultivating|Improvising|Running "; then
31
+ busy_seen=1; idle_count=0
32
+ elif [ "$busy_seen" = 1 ]; then
33
+ idle_count=$((idle_count+1))
34
+ if [ "$idle_count" -ge 3 ]; then
35
+ tmux capture-pane -t <sess> -p > "$OUT"; echo "DONE $(date) after $(( now - start ))s" >> "$LOG"; exit 0
36
+ fi
37
+ fi
38
+ sleep 10
39
+ done
40
+ EOF
41
+ chmod +x ~/cc-watch.sh
42
+ setsid nohup bash ~/cc-watch.sh >/dev/null 2>&1 < /dev/null &
43
+ sleep 2; ps aux | grep -v grep | grep cc-watch.sh'
44
+ ```
45
+
46
+ ## 2. Block until the marker appears, then return the final screen
47
+
48
+ ```bash
49
+ ssh <ssh-user>@<remote-host> '
50
+ for i in $(seq 1 900); do
51
+ if grep -qE "DONE|TIMEOUT" ~/cc-watch.log 2>/dev/null; then
52
+ echo "=== WATCHER LOG ==="; cat ~/cc-watch.log
53
+ echo; echo "=== FINAL CAPTURE ==="; cat ~/cc-final.txt 2>/dev/null
54
+ exit 0
55
+ fi
56
+ sleep 10
57
+ done'
58
+ ```
59
+
60
+ If the blocking loop times out before the marker appears, run it again (or, on a persistent
61
+ session, poll again with `input_command`) — the watcher keeps running on the remote regardless.
62
+
63
+ ## Notes
64
+
65
+ - `tail -30` inspects only the live bottom region of the pane, so an old `Cultivating` line scrolled
66
+ up into history can't be misread as "still busy".
67
+ - Requiring 3 consecutive idle checks avoids a false "done" during a brief pause between tool calls
68
+ within one turn.
69
+ - The user can read the result themselves at any time: `cat ~/cc-final.txt`.
@@ -0,0 +1,46 @@
1
+ # Persistent SSH session — expect template
2
+
3
+ Full template for mode 1 (persistent SSH session). Write it to your scratchpad
4
+ (`<app_data_dir>/agents/<agent_id>/scratchpad/remote_shell.exp`) and fill the placeholders at
5
+ runtime — never commit real values, and have expect read the password from the environment rather
6
+ than baking it into the file.
7
+
8
+ ```expect
9
+ #!/usr/bin/expect -f
10
+ set timeout 30
11
+ log_user 0
12
+ set env(TERM) xterm-256color ;# must be set before spawn — see the TERM note below
13
+ spawn ssh -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 \
14
+ -o ServerAliveCountMax=3 -o ConnectTimeout=15 <ssh-user>@<remote-host>
15
+ expect {
16
+ "yes/no" { send "yes\r"; exp_continue }
17
+ "password:" { send "$env(REMOTE_SSH_PASSWORD)\r"; exp_continue }
18
+ eof { puts "\n=== SSH CLOSED ==="; exit 1 }
19
+ }
20
+ sleep 2
21
+ send "su - <target-user>\r" ;# drop this block when no user switch is needed
22
+ expect {
23
+ "Password:" { send "$env(REMOTE_SSH_PASSWORD)\r"; exp_continue }
24
+ timeout { }
25
+ }
26
+ sleep 1
27
+ send "echo PERSISTENT_SESSION_READY; whoami; hostname; pwd\r"
28
+ expect "PERSISTENT_SESSION_READY"
29
+ log_user 1
30
+ set timeout -1
31
+ interact
32
+ ```
33
+
34
+ ## Operating notes
35
+
36
+ - Start it with `exec_command` and let it keep running — the closing `interact` holds the
37
+ connection open, and its `process_id` is your handle: send remote commands with `input_command`
38
+ (write to stdin) and poll the same session for new output.
39
+ - The `sleep`-based waits replace prompt-regex matching on purpose: matching shell prompts
40
+ (`[$#] $` and friends) breaks on MOTD/ANSI noise and leaves the script stuck in `expect`.
41
+ - `set env(TERM) xterm-256color` before `spawn`, or the remote shell sees `TERM=dumb` and TUIs
42
+ render badly.
43
+ - Probe before relying on the session (`echo PING; whoami`), and reuse it for most remote commands
44
+ — open extra one-shot connections sparingly.
45
+ - When finished: stop the expect process you started (and its ssh child; leave other people's
46
+ sessions alone) and delete the script from the scratchpad.