Skip to content

Add @agent-socket/shell: an agent runs commands on your machine, each approved in your terminal - #3

Merged
repalash merged 3 commits into
masterfrom
shell-cli
Oct 30, 2026
Merged

repalash merged 3 commits into
masterfrom
shell-cli

Conversation

@repalash

Copy link
Copy Markdown
Collaborator

What

npx @agent-socket/shell lets an agent anywhere (a cloud Claude session, a chat with a code sandbox) run shell commands on your machine through an agent-socket link. It needs no agent installed locally and no SSH. Every command waits in your terminal:

── 2 · 14:02:11 ──────────────────────────────
why: remove the old Xcode build products (31 GB)
$ rm -rf ~/Library/Developer/Xcode/DerivedData
Enter run · n refuse · e edit and run · a auto · q end
  • Enter runs it. The output shows in the terminal and goes back to the agent.
  • n refuses it, with an optional note to the agent. The note is how you steer it without going through the chat.
  • e edits the command and runs what you typed. The result says what actually ran.
  • a switches to auto: commands run as they arrive and are still shown. s goes back to step.
  • q or Ctrl-C at the prompt ends the session, as does the timer (60 min by default). The link stops working at once.

The agent gets two tools, GET /info and POST /run. /run takes the command as plain text with an x-why header, or JSON. It answers within about 25 s, or with a 202 task to poll. The result is plain text: status, exit code, seconds, ran:, note:, then the output, cut in the middle to fit the relay's 64 KiB task limit. shell/agents.md is the briefing, and it includes a small run shell function for agents that work in a sandbox.

The design is deliberately minimal. There's no command parsing, rules engine, AI reviewer or sandbox, because the agent's own harness already decides what to try and you decide what runs. There are no relay changes either: it's an ordinary SDK app, with the SDK vendored as in gpu-browser.

Files

  • shell/: the CLI (bin/agent-socket-shell.mjs), agents.md, README, package.json, the vendored SDK and scripts/vendor-sdk.sh. npm run build now vendors it too.
  • harness/scenarios/64-shell.mjs: runs the real CLI in a pseudo-terminal (util-linux script) against the relay, typing keys like a person and calling the link like an agent.
  • docs/plans/shell.md: the plan, how commands run, and the macOS menu bar app as the next step.
  • Root README section, harness README entry, CHANGELOG entry.

Behaviour worth reviewing

  • Commands aren't detached. They stay in the terminal's foreground process group, so sudo can ask for the password in the terminal and Ctrl-C works as in a shell. As in a terminal, Ctrl-C doesn't stop the command's own background jobs (cmd &). A timeout does stop the whole tree.
  • Background jobs don't hold up the result. If a background job keeps the output pipe open, reading stops 1 s after the command exits.
  • Agent text is shown safely. Control, bidi and zero-width characters in the command or the reason are shown as visible \u{…} escapes with a warning.
  • Dropped connections. If the relay connection drops, commands that were still waiting on a direct answer are dropped, because the agent already got a 503. Async answers are kept and sent again after the reconnect.

Test plan

  • node harness/run.mjs 64 passes (22 checks): Enter, both the direct and the 202-task path; refusing with a note; edit; a queue of three in a given cwd; Ctrl-C; timeout leaving nothing running; long output keeping head and tail under the task cap; an escape-sequence command shown escaped; auto and back to step; a bad request getting 400; q exiting and the link answering 503 with the session ended rather than held.
  • Run on macOS. Only tested on Linux so far, including the check that pausing Node's stdin lets a command read the terminal (sudo).
  • A real session from a cloud agent, such as cleaning up a full Mac disk.
  • CI: scenario 64 runs with the rest of the harness and needs script, pgrep and pkill, which Ubuntu runners have.
  • Not on npm yet (@agent-socket/shell 0.1.0).

🤖 Generated with Claude Code

https://claude.ai/code/session_01UR8BTB2vTAfKgbiwPGD12P

… approved in your terminal

`npx @agent-socket/shell` prints a link for the agent's chat. Every command
the agent sends to POST /run shows up in the terminal with its reason and
runs only on Enter; n refuses it with an optional note back to the agent,
e edits and runs it, a switches to auto. Results go back as plain text
(status, exit, seconds, ran, note, truncated, then the output), directly
within ~25 s or as an async task. Control, bidi and zero-width characters
in agent text are shown as visible escapes. q / Ctrl-C at the prompt / the
timer end the session so the link dies at once.

Harness scenario 64 drives the real CLI in a pseudo-terminal against the
relay. docs/plans/shell.md has the plan and the macOS menu bar app as the
next step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UR8BTB2vTAfKgbiwPGD12P
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T00:14:38.860680Z e09af71 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23b8c0d639

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread shell/bin/agent-socket-shell.mjs Outdated
out()
out(cyan(`── ${call.n} · ${time} `) + dim("─".repeat(30)))
if (call.why) out(`why: ${escapeHidden(call.why)}`)
if (call.cwd !== START_DIR) out(dim(`in ${call.cwd}`))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape the agent-supplied cwd before displaying it

When a JSON /run request supplies a cwd containing escape, bidi, or other control characters, this line writes it verbatim before approval, unlike the sanitized why and command. A caller holding the bearer link can therefore clear or rewrite the terminal or emit OSC sequences, making the approval display differ from the requested action even in step mode; render cwd through the same escaping and warning path.

Useful? React with 👍 / 👎.

Comment on lines +409 to +410
const lost = queue.filter((c) => !c.async)
for (const c of lost) { queue.splice(queue.indexOf(c), 1); c.finish({ status: "error", error: "connection dropped" }) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel the active synchronous call on disconnect

If the WebSocket drops before HOLD_MS while a call is displayed or executing, pump() has already shifted that call out of queue, so this filter never marks it lost even though call.async is still false. The relay returns 503 and tells the agent to retry, while the stale call can still be approved or finish locally; the retried request can then execute the same destructive operation a second time. Track and cancel the active synchronous call as well as queued calls when disconnecting.

Useful? React with 👍 / 👎.

Comment thread shell/bin/agent-socket-shell.mjs Outdated
Comment on lines +118 to +121
while (Buffer.byteLength(JSON.stringify(text)) > MAX_JSON) {
const keep = Math.floor(output.length * 0.8 / 2)
const cut = output.length - 2 * keep
output = output.slice(0, keep) + `\n[… ${cut} more characters cut …]\n` + output.slice(-keep)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound metadata while shrinking oversized results

This loop only shrinks output, so it never terminates when the fixed header fields alone exceed MAX_JSON. For example, an oversized agent-supplied nonexistent cwd is copied into error, or a user edits a near-100,000-character command and it is copied into ran; with empty output the serialized result remains over the cap forever and the CLI's event loop hangs. Truncate or cap all result fields, and stop iterating when shrinking output no longer reduces the payload.

Useful? React with 👍 / 👎.

shaderpal and others added 2 commits October 30, 2026 00:12
CHANGELOG: 0.4.0 shipped without the shell, so its entry moves to Unreleased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UccXUXqFpdT4o43WU13DhA
…e link drops, bound the result

From the Codex review of #3:
- The cwd an agent sends was printed raw before approval, so escape
  sequences in it could rewrite what you read. It is now shown escaped like
  the command and the reason, with the hidden-characters warning, and a cwd
  over 4,096 characters is refused with 400.
- When the relay link dropped, only queued calls were dropped. The call on
  screen could still be approved after the agent got its 503 and was told
  to retry, so the command could run twice. It is now withdrawn if it
  hasn't started; if it was already running, the CLI says the agent didn't
  get the result.
- Shrinking a result only cut the output, so oversized metadata (a huge
  cwd in an error, a long edited command in `ran`) spun forever. Every
  field is capped and the loop stops when the output can't shrink.

Scenario 64 covers all three; each new check fails on the previous CLI.
Scenario 58 took the token's verifier with split("_")[2], which is empty
when the verifier itself starts with "_"; it now slices after the prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UccXUXqFpdT4o43WU13DhA

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e09af71e7b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fs.writeFileSync(file, text + "\n", { mode: 0o600 })
keyboard(false)
const editor = process.env.VISUAL || process.env.EDITOR || "vi"
spawnSync(SHELL, ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the event loop running while the command editor is open

When a user edits a multiline command and spends more than 30 seconds in the editor, spawnSync blocks the CLI's event loop, preventing the /run handler's 25-second timer from returning a task ID. The relay's independent timeout in relay/src/relay-do.ts then returns 504 tool_timeout, but closing the editor still executes the edited command locally. The agent receives no result and may retry an operation that already ran. Launch and await the editor asynchronously so task promotion and WebSocket processing continue during editing.

Useful? React with 👍 / 👎.

cancelPrompt = () => { cancelled = true; out(); rl.close() }
rl.question(prompt, (answer) => { rl.close(); resolve(answer) })
rl.on("close", () => { lineMode = false; cancelPrompt = null; keyboard(true); if (cancelled) resolve(null) })
if (prefill) rl.write(prefill)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve terminal escaping when prefilling the edit prompt

When a single-line command contains terminal control sequences and the user presses e, the original agent-supplied command reaches rl.write(prefill) without escaping. Node's readline writes those control bytes to the terminal, so an escape sequence that was safely displayed in the approval view can clear or rewrite the edit prompt immediately before the user submits the command. This is reproducible with a prefill containing \x1b[2K\x1b[1G. Use an editing path that does not render the original hidden characters verbatim, while preserving the command's actual contents.

Useful? React with 👍 / 👎.

Comment on lines +412 to +413
await closed
process.exit(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Finish process termination before exiting the CLI

If the session expires while a command ignores SIGTERM, end() calls killTree(), but its SIGKILL escalation is scheduled for two seconds later. Once the WebSocket closes, this unconditional process.exit(0) terminates the CLI before that escalation can run, leaving the command running after the session has ended. Await process-tree termination, including the escalation when necessary, before exiting.

Useful? React with 👍 / 👎.

@repalash
repalash merged commit e09af71 into master Oct 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants