remote-claude-code
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.
git clone --depth 1 https://github.com/Prism-Shadow/penguin-harness /tmp/remote-claude-code && cp -r /tmp/remote-claude-code/packages/skills/skills/remote-claude-code ~/.claude/skills/remote-claude-codeSKILL.md
# Remote Claude Code Drive Claude Code on a remote Linux host over SSH. Three modes, in increasing interactivity: 1. **Persistent SSH session** — one long-lived expect-driven connection you keep feeding commands across turns. 2. **Headless (`claude -p`)** — one-shot or scripted calls, with the stdin fix that naive invocations need. 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. 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). ## Before you start 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. Credential rules — non-negotiable: - 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. - **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. - Prefer SSH keys when they are already set up — then no password tooling is needed at all. - Back up remote config before modifying it, and never copy remote credential stores (`~/.claude/.credentials.json`, OAuth fields of `~/.claude.json`, private keys) anywhere. ## 1. Persistent SSH session (expect) 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. 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. ## 2. Headless Claude Code (`claude -p`) — the stdin gotcha `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`: ```bash claude -p "Reply with exactly: all good" < /dev/null ``` 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. Symptom checklist: process running + no output + no TCP connections → stdin starvation; add `< /dev/null`. ## 3. Interactive TUI — tmux is the way 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: ```bash # start detached, then read the screen TERM=xterm-256color tmux new-session -d -s <sess> "claude" sleep 8 tmux capture-pane -t <sess> -p | tail -25 # converse: type the message, confirm it landed, submit — text and Enter as separate calls (§3.3) tmux send-keys -t <sess> -l "Introduce yourself in one sentence" tmux capture-pane -t <sess> -p | tail -5 # exactly your message on the input line? tmux send-keys -t <sess> Enter sleep 20 tmux capture-pane -t <sess> -p -S -200 # scrollback: a long reply overflows the visible pane ``` - 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. - 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. - `TERM=xterm-256color` must be visible to the tmux **server** (prefix the `tmux new-session` call that st
Use when developing PenguinHarness itself — changing packages/{core,server,web,cli,desktop,landing,docs,skills}, the built-in model catalog, the installers or the release workflow; writing or auditing changelog entries; writing a blog post or capturing release screenshots; deciding what to do about data already on disk; or auditing prose that reads like a leaked authoring session. Covers the two-repo symlink layout, the CI-parity verification chain, the record-and-ship contract, where blog media is hosted, and the seams that are intentional.
Use when changing the PenguinHarness Web App (`packages/web`) — adding or restyling any UI, picking a status colour, adding an icon, laying out a row or a form field, writing user-facing copy, or building a popup. Covers the semantic tone tokens, the icon size/stroke/gap scale, the semantic-versus-formatting rule for explanatory text, the two-dictionary i18n contract, and the portal-panel pattern with its Esc and scroll caveats.
Run one specified Test Agent on one specified Benchmark Case exactly once, privately score that execution, and return one protocol result.
Initialize an Agent's settings from a user requirement by writing AGENTS.md, setting identity metadata, and installing only needed Skills.