Let scripts and AI reuse channels you already opened

MyShell can expose a small HTTP endpoint bound to 127.0.0.1. Local scripts — or the AI assistant you are already using — can run commands over SSH channels you authenticated yourself, without ever getting your password or key. Off by default.

What it is for

To let an AI assistant check a server log, you would normally have to hand it a password or key.

With the local API, you connect in MyShell, and local programs run commands over that existing channel without ever seeing the credentials. It works for machines with one-time passwords or two-factor auth too.

Security boundary

  • Loopback only. Bound to 127.0.0.1; unreachable from the LAN or the internet.
  • Every request needs the key. It lives in a file only your macOS user can read, and can be regenerated on every launch.
  • Browsers are refused. Any request carrying an Origin header is rejected, so a web page cannot call it through your browser.
  • It will not take credentials. The connect endpoint accepts only a saved connection id; passwords, keys or passphrases in the body are rejected outright. Changing auth means going back into the app.
  • It will not hand credentials out. Neither the channel list nor the self-description contains any secret field.
  • It ships disabled. Leave it that way when you are not using it — this API can run commands on every channel you currently have open, which is both its point and its risk.

Turning it on

Settings → Local API → enable. The page shows the current address, the path to the runtime file, and a cURL snippet you can paste straight into a terminal.

Once on, MyShell writes a runtime file (local-api.json) holding the address and the current key. Callers should read the address from that file — do not hardcode the port; the app picks another one if the preferred port is taken.

Disabling the endpoint or quitting the app removes the file and cancels any queued commands.

Setting up a caller

Keep the key in a process variable. Do not echo it, and do not put it in a script or your shell history.

INFO="$HOME/Library/Application Support/uk.flyooo.myshell/local-api.json"
BASE_URL="$(jq -r '.baseUrl' "$INFO")"
TOKEN="$(jq -r '.token' "$INFO")"

Endpoints

Your first request should be GET /v1. It returns every route this build supports, with parameters, limits and error codes — so a caller can adapt instead of hardcoding what a document said.

  • GET /v1 — Self-description: routes, parameters, response shapes, error codes. Never the key.
  • GET /v1/health — Whether the service is up. Says nothing about available channels.
  • GET /v1/connections — Saved SSH connections the app can open. No credential fields.
  • GET /v1/hubs — Live channels: name, address, user, OS, shell, how busy each one is.
  • POST /v1/hubs/connect — Open a headless background channel for a saved connection, or reuse a live one.
  • POST /v1/hubs/exec — Run a command on a channel; returns stdout, stderr, exit code and timings.
  • POST /v1/hubs/exec-by-name — Same, addressing the channel by name. The name must be unique.
  • POST /v1/hubs/keep-open — Keep a channel alive when no terminal is attached.
  • POST /v1/hubs/close — Close a channel.

A few examples

See what channels exist — Start by checking whether something is already connected.

curl --fail --silent --show-error \
  --header "Authorization: Bearer $TOKEN" \
  "$BASE_URL/v1/hubs" | jq

Run a command on a channel — Everything goes in the JSON body, so a channel name with non-ASCII characters needs no escaping.

BODY="$(jq -nc --arg hubId "$HUB_ID" --arg command "uptime" \
  '{hubId: $hubId, command: $command, timeoutMs: 30000}')"

curl --fail --silent --show-error --request POST \
  --header "Authorization: Bearer $TOKEN" \
  --header 'Content-Type: application/json' \
  --data-binary "$BODY" \
  "$BASE_URL/v1/hubs/exec" | jq

Open a background channel for a saved connection — The body takes a connection id and nothing else. Smuggling in a password or key is rejected.

BODY="$(jq -nc --arg connectionId "$ID" '{connectionId: $connectionId}')"

curl --fail --silent --show-error --request POST \
  --header "Authorization: Bearer $TOKEN" \
  --header 'Content-Type: application/json' \
  --data-binary "$BODY" \
  "$BASE_URL/v1/hubs/connect" | jq

Things callers get wrong

  • A 200 does not mean the command succeeded. Check the HTTP status, the status field and exitCode together. A command that fails on its own (say exit 1) still returns 200.
  • Output is not always text. When the remote output is not valid UTF-8 it comes back Base64-encoded with the encoding flagged; check that field before displaying or decoding.
  • Each channel queues at most 32 commands. A full queue returns 429 — back off rather than hammering.
  • Request bodies cap at 64 KiB, a single command at 16 KiB.
  • Prefer read-only checks first. Anything that installs, deletes, restarts a service, edits config or writes files should be explained to the user before it runs.

Not connected yet? Start with the guide →