Documentation
Technical details behind AI Commander, your AI command center for remote computer management: the agent, relay, session codes, MCP tools, REST API, desktop apps, and security model.
Overview
AI Commander lets an AI client run shell commands on machines you own — without exposing inbound ports, SSH, or a VPN. It has two parts:
- The relay — a hosted endpoint at
https://aicommander.devthat speaks both MCP and a REST API. Your AI client talks to this. There is nothing to deploy or host. - The agent — a small program on each machine you control: a Linux systemd service or a bundled macOS / Windows desktop app. It dials out to the relay over an encrypted WebSocket, so nothing listens for inbound connections on the machine.
Each machine is addressed by a stable session code like AIC-7K3P-WX9M-RTBN. The relay routes a command for that code to the matching agent, which runs it and streams stdout/stderr back. The same core operation — run a command by code — is available through MCP tools, the REST API, and an installable Agent Skill.
End to end, a command flows client → relay → agent and the output streams back the same way. The relay never stores command text or output — see Security.
The Linux agent
On Linux (servers, VMs, Raspberry Pi) the native install is a single standalone binary — no Node or npm — run as a systemd service that starts on boot and opens an outbound WebSocket to the relay. Because it runs as root, the install guide downloads a versioned installer plus detached signature, verifies it before sudo, and only then executes the verified local file. The old curl …/install | sudo bash flow is intentionally unsupported.
The same agent is also published on npm as @aicommander/agent for Node-managed environments (dev boxes, containers, CI; needs Node ≥18 on the target). Run it ephemerally in the foreground with npx @aicommander/agent — it registers and prints the full session code you hand to your AI client (Ctrl-C to stop — no service; no --reveal needed since you ran it yourself). Or install it as the same systemd service with sudo npm i -g @aicommander/agent followed by sudo aicommander-agent install. Either way you get the same agent, the same aicommander-agent.service unit, the same controller commands, and the same stable session code.
aicommander.dev cannot protect against compromise of that same origin.Once running it registers with the relay and produces a stable session code. Logs and normal status output mask the code; reveal the full value only when you need to copy it:
$ sudo aicommander-agent status --reveal
# Output:
Session: AIC-7K3P-WX9M-RTBN
Supported targets: Linux x86_64 and Linux arm64 (e.g. Raspberry Pi, ARM servers) — each creates the same systemd unit, aicommander-agent.service.
Controller commands
The agent binary doubles as its own service controller:
$ sudo aicommander-agent status # masked code, uptime, state
$ sudo aicommander-agent status --reveal # print the full session code
$ sudo aicommander-agent change-code # mint a NEW code — old code dies, clears ALL links & blocks (prompts; --yes to skip)
$ sudo aicommander-agent list-admins # accounts linked to this machine (masked emails)
$ sudo aicommander-agent block-admin 2 # block one account by list number / id (any time)
$ sudo aicommander-agent unblock-admin 2 # unblock to restore that account's access
$ sudo aicommander-agent disable # stop and disable on boot
$ sudo aicommander-agent uninstall --force
list-admins shows every account currently linked to this machine; emails are masked (e.g. lu****@wear****.com) and never leave the relay unmasked. block-admin blocks one account at any time — it refuses that account's access and stops it re-linking, but the account stays listed (in a "Blocked" section) so unblock-admin restores it instantly. To lock everyone out at once, reset the code instead. See Security.
Session code lifetime
Session codes are stable: generated once at install, reused across reboots, service restarts, reconnects, and updates. They do not auto-rotate or expire on a timer. A code changes only when you run aicommander-agent change-code (Linux) or click "Reset Access Code" in the desktop tray, and it is removed only on a full uninstall.
change-code / "Reset Access Code" (see Security).Desktop apps
On macOS and Windows the agent ships inside a lightweight menu-bar / tray application — there is no command-line agent installer for those platforms. The app bundles the agent, so launching it registers the machine and there's no terminal involved. From the menu-bar / tray icon you can:
- Copy the current session code — it's shown right in the icon's menu.
- Reset the code ("Reset Access Code") — confirms first, since it resets everyone's access (mints a new code and clears all links and blocks).
- Review and block accounts via "Linked Accounts…" — lists every account linked to the machine (masked emails) and lets you block or unblock any of them at any time.
- Enable or disable remote access with a single click.
- Get notified when a remote operator connects — a brief alert (rate-limited so steady use isn't noisy) with a one-click Disable Remote Access.
The code is persisted in the app's user-data directory, survives app updates, and is removed only on a full uninstall. The status indicator reflects the agent's connection:
- Connected — agent is running and reachable.
- Reconnecting — agent is trying to re-establish the WebSocket.
- Disabled — access is paused; the agent will not accept commands.
Download links (macOS Apple Silicon / Intel, Windows x64) are on the home page and in the Connect guide.
Session codes
Session codes are high-entropy identifiers that map an online agent to a relay session. Format: AIC-XXXX-XXXX-XXXX — three groups of 4 characters drawn from an unambiguous alphabet (no I, L, O, or U), giving ~59 bits of CSPRNG entropy. Example: AIC-7K3P-WX9M-RTBN. A code is generated once at install and stays stable across reboots and restarts.
How clients use them
An AI client recognizes the AIC- prefix in conversation and routes commands to the right machine via remote_exec. There is no per-machine MCP config — you use the current code for the machine you want to reach. With an account you can use a friendly alias instead of the raw code.
Multiple machines
You can have any number of agents registered simultaneously. Each gets its own code and its own isolated session. A command sent to AIC-7K3P-WX9M-RTBN is routed only to that session, not to AIC-9F4D-2HQN-VK7T.
Sharing codes
A session code is sensitive. Anyone who knows a current code can run commands on that machine, so only share codes with people and agents you trust. To give someone ongoing access, share the current code and have them sign in. To cut access, either block a specific account from the machine itself — aicommander-agent block-admin / unblock-admin or the desktop tray's "Linked Accounts…" — or reset the code with change-code. Resetting is the master "start over" action: the old code stops working immediately and all links and blocks are cleared.
MCP tools
The relay speaks standard Model Context Protocol over HTTP with SSE responses at https://aicommander.dev/mcp. Adding it to a client (see Connect) exposes the tools below. Connecting to /mcp is account-bound by default: the client runs a one-time browser sign-in — it auto-registers via Dynamic Client Registration, so there are no manual credentials to paste — and the connection binds to your account, so your saved aliases resolve. An unauthenticated request gets an HTTP 401 with a WWW-Authenticate challenge, which is what kicks off that sign-in. To drive a machine by raw AIC-… code without signing in, connect to https://aicommander.dev/mcp?anonymous=1 instead (allowed during the code's first hour). Each tool accepts a code that is either a raw AIC-… session code or, on a signed-in connection, one of your saved aliases.
remote_exec(code, command, cwd?, timeout_ms?)
Runs a shell command on the machine for code and returns the result.
code— the target machine's session code or alias.command— the shell command to run.cwd(optional) — working directory for the command.timeout_ms(optional) — per-command timeout; the command is killed if it overruns.
The buffered result has the shape:
{ "ok": true, "exitCode": 0, "stdout": "…", "stderr": "…", "truncated": false }
Streaming output arrives as SSE message events as the command runs; the stream closes with the exit code. Total output is capped to protect the relay — oversized output is truncated (truncated: true) and the command is killed. Output is delivered only to the connection that issued the command.
session_status(code)
Reports whether the machine for code is online, without running anything. Anonymous callers can use this during the code's first hour; an account works after that. Unknown and stale codes return the same message, so there is no code-existence oracle.
list_machines() — requires an API key
Lists every machine linked to your account, each with its alias and live online status. Only available with an account credential (Authorization: Bearer <api-key>, or AICOMMANDER_TOKEN for the @aicommander/mcp stdio bridge), since it's scoped to your machines.
REST API
The same operations are exposed as a plain REST API for clients that don't speak MCP — or for a single HTTP call from a script, a cron job, or any chat model with a fetch/browse tool. The machine-readable spec is at /openapi.json.
Authentication
This REST surface has its own auth, separate from the MCP endpoint. No token means anonymous access during the code's first hour. Authorization: Bearer <api-key> enables alias-based access for your account (kept active by opening the dashboard within the last 24h — see Security). There is no OAuth browser flow on this surface; REST clients paste an API key.
GET /api/v1/status/{code}
Returns whether the machine for {code} is online. Works anonymously during the code's first hour.
$ curl -s https://aicommander.dev/api/v1/status/AIC-7K3P-WX9M-RTBN
POST /api/v1/exec
Runs a command and returns a buffered JSON result. Body: { "code", "command", "cwd"?, "timeout_ms"? }.
$ curl -s -X POST https://aicommander.dev/api/v1/exec \
-H 'Content-Type: application/json' \
-d '{"code":"AIC-7K3P-WX9M-RTBN","command":"df -h"}'
The response mirrors remote_exec: { "ok": true, "exitCode": 0, "stdout": "…", "stderr": "…", "truncated": false }. Send Accept: text/event-stream to stream the result instead of buffering it.
GET /api/v1/status — requires an API key
The token-authed counterpart to the list_machines tool: with Authorization: Bearer <api-key>, returns every machine on your account with its alias and live online status.
POST /api/v1/secure-exec — sandboxed, non-root, service-token only
Runs an allowlisted, sandboxed, non-root command on the one machine a service token is pinned to. Body: { "argv": [...], "input"?: base64, "cwd"?, "timeout_ms"? } — argv is an exec-style vector (no shell). This is the deliberate opposite of /api/v1/exec: it never reaches the root command surface, and the only accepted credential is a service token (aics_…) — an account API key, admin token, or anonymous are rejected.
$ curl -s -X POST https://aicommander.dev/api/v1/secure-exec \
-H 'Authorization: Bearer aics_…' \
-H 'Content-Type: application/json' \
-d '{"argv":["git","status","--porcelain"],"cwd":"/repo"}'
The response mirrors /api/v1/exec ({ "ok": true, "exitCode": 0, "stdout": "…", "stderr": "…", "truncated": false }, output capped at 8 MiB). A service token is long-lived and revoke-only (no expiry), scoped to one machine + an allowlist of command basenames, and bypasses the 24h dashboard re-activation gate.
Token creation returns 400 secure_exec_command_denied for known generic execution primitives: common shells; Python/Node/Perl/Ruby/PHP/Lua runtimes (including common version-suffixed names); env/busybox/find/xargs/awk/sed runners; sudo/su/doas/pkexec; Docker/Podman/nerdctl; and kubectl (whose plugin and exec surfaces can dispatch other programs). The agent applies the same shared policy before spawn even if the relay sends a hostile allowlist.
Before spawning, the Linux agent also refuses the aicommander-exec account when its primary or supplementary groups include root, wheel, sudo, docker, podman, lxd, incus, disk, libvirt, or numeric gid 0. Unreadable, missing-primary, malformed or duplicate local passwd/group data fails closed. This denylist is deliberately not complete: the dedicated non-root uid/group boundary—not command-name classification—is the containment mechanism.
/api/v1/secure-tokens & /api/v1/secure-exec/audit — requires an API key
Create service tokens manually in the dashboard, where the plaintext token is shown once. Account API keys can manage existing tokens but cannot mint new long-lived credentials: POST /api/v1/secure-tokens returns 403 with a dashboard link; GET /api/v1/secure-tokens lists tokens (no secrets); DELETE /api/v1/secure-tokens/{id} revokes; GET /api/v1/secure-exec/audit lists recent runs (command basename, exit code, duration, caller IP, bytes, truncated, time — never arguments, input, or output).
SKILL.md into your agent's skills directory. The agent auto-loads it and drives machines over the REST API, with no MCP server to add and no OAuth. Setup: Connect → REST API & Skill; view it at /skill/SKILL.md.Security
The access model is simple: a session code is the credential for its machine. Anyone who knows a current code can run commands on that machine — for as long as the code stays valid — until the owner resets the code or blocks their account. The code is stable (it doesn't auto-rotate); it's minted/refreshed only when someone with physical/admin access installs the agent or runs change-code.
Who can use a code
- Sign in for ongoing access. With an account — personal API key or magic-link OAuth — first use auto-links the machine and saves it under an alias. You can then use the alias or raw code, subject to the daily key re-activation gate. If the first link happens within the code's first hour, access starts immediately. If it happens later, the account is linked but starts blocked pending operator approval, because the code may have leaked by then. Access lasts until the owner resets the code or blocks your account.
- No account needed for the first hour. During the first hour after a code is created or refreshed, anyone can paste the code and go. After that, access without an account stops working and
remote_exec/session_statusask the user to sign in or have the owner reset the code. - Giving someone access: share the current code and have them sign in. A signed-in account can also add a machine from the dashboard (
POST /api/machines) with a code of any age. - Blocking an account (owner-side): from the machine itself, the owner can list linked accounts and block or unblock one account with
aicommander-agent list-admins,block-admin,unblock-admin, or the desktop tray's "Linked Accounts…". Emails are masked server-side (e.g.lu****@wear****.com). Blocking refuses that account's access and stops it re-linking; unblocking restores it. To remove everyone at once, reset the code, which clears all links and blocks.
| Component | Details |
|---|---|
| Session code | AIC-XXXX-XXXX-XXXX, generated from a high-entropy CSPRNG with an unambiguous alphabet. The credential for a machine. Stable across reboots/restarts; changes only on change-code, removed only on uninstall. Anyone who knows it can use it while access is allowed — see above. |
| Codes at rest | The plaintext code is never stored. Every record is keyed by a one-way hash of the code, so a database leak contains no usable codes and they can't be brute-forced from it — the only exception being the first two characters of an account-linked machine's code, kept as a non-secret pairing fingerprint that can't reconstruct or use the code. |
| Agent tokens | Minted fresh per registration and stored only as hashes. The agent rotates its token automatically while idle (never mid-command), so any single token is short-lived even if leaked. |
| Daily re-activation | An account's API keys stay dormant until the owner opens the dashboard, and re-arm each time the dashboard is opened (roughly once a day). A key sitting in a config file can't act on its own without recent dashboard access. On by default; an account can opt out in the dashboard (flagged as risk-increasing). |
| Accounts & API keys | Passwordless sign-in by passkey or email magic link. API keys are stored as salted hashes and are revocable from the dashboard. Used as the Bearer token to reach machines by alias. |
| OAuth (MCP clients) | Passwordless OAuth (OAuth 2.1 + PKCE): the user enters an email code or uses a passkey, and the access token binds to their account so aliases work. Every approval appears under Connected apps; disconnecting it immediately blocks both access and refresh tokens. Refresh tokens rotate atomically, detect reuse, and never extend beyond 30 days from the original approval. |
| Platform secrets | Stored in a dedicated secret store — never in code or any database, never returned by any API. Secret comparisons are constant-time (no timing side-channel). |
| Rate limiting | Agent registration and /mcp tool calls are rate-limited per IP (with a global ceiling) — covering session_status too, since it is a brute-force oracle for codes. Limits are enforced atomically to resist races and IP rotation. Magic-link sends are limited per IP, per email, and by a global domain-wide ceiling. Anonymous lookups return an identical message for not-found vs. stale codes, so there is no code-existence oracle. |
| CORS & CSRF | Credentialed CORS is locked to the dashboard origin for cookie-authed routes only; everything else is token- or IP-authed. Mutating dashboard requests carry an X-Requested-With/Origin check on top of a SameSite=Lax session cookie. |
| Cleanup job | A scheduled cleanup job purges expired web sessions, magic links, old revoked API keys, and expired OAuth token-family rows. Consumed refresh predecessors stay through the family's fixed absolute expiry so replay detection remains effective. |
| No command logging | We never log or persist payloads. Commands and their stdout/stderr are never written to logs or any database — no command text or output is stored at all. Data is processed only transiently in the relay's memory while it streams through, then discarded. |
| Output isolation & caps | A command's output is delivered only to the connection that issued it — another connection on the same session never sees it. Total output is capped to protect the relay from memory exhaustion (oversized output is truncated and the command is killed). |
| Encryption in transit | All connections — agent↔relay and client↔relay — run over TLS to the edge (wss:// / HTTPS). This is not end-to-end: the relay processes command text and output in plaintext in memory only long enough to forward it — it is never written to disk or any store. |
| Safe defaults / opt-in | Higher-reach features are off by default and must be explicitly enabled by the machine's owner — e.g. desktop screen sharing (remote_screenshot) is disabled until toggled on in the tray. The desktop app also notifies the owner by default when a remote operator connects. |
| Install integrity | Release CI signs a versioned installer and each Linux agent binary with Ed25519. The documented flow verifies the installer before sudo; the verified installer pins its release and enforces both the binary's SHA-256 checksum and detached signature. Missing OpenSSL 3, LibreSSL, missing pkeyutl -rawin, missing key/signature, or verification failure all abort. This protects against altered CDN artifacts only when the public-key fingerprint was obtained independently; the website cannot authenticate its own trust anchor. |
| Prompt-injection | The MCP tools instruct the model to treat command output strictly as untrusted data to relay back — never as instructions to act on — so a malicious file or log line can't hijack the agent. |
| Web hardening | Explicit OAuth consent, optional Turnstile, strict security headers, and a CSRF check on top of the SameSite=Lax dashboard cookie. Every cookie-authenticated dashboard API response (including failures and preflights) is no-store, varies on Origin and Cookie, and exposes credentialed CORS only to the dashboard origin. Static executable scripts are same-origin only; feedback opens an email instead of loading a third-party SDK. OAuth loopback completion is scriptless and can frame/navigate only to its validated local callback origin, while hosted callbacks remain redirects. |
| Session isolation | Each session is a separate, fully isolated session instance. One session cannot read another's traffic. |
| Network | Agent connects outbound only via WebSocket. No inbound ports are opened on the target machine. |
| Root access | The agent runs as root by default for administration workflows. The machine owner controls who can act on the session code (by resetting the code or blocking accounts). To restrict command permissions, run the agent as a non-root user. |
Security FAQ
Do you log or store my commands and their output?
No — this is a guarantee, not best-effort. We never log or persist payloads. Commands and their stdout/stderr are never written to logs or any database; no command text or output is stored at all. Data is processed only transiently in the relay's memory while it streams through, then discarded. We keep no command history.
Is the connection encrypted?
Yes, in transit: every link (agent↔relay and client↔relay) runs over TLS to the edge. It is not end-to-end encrypted, though — the relay handles command text and output in plaintext in memory in order to forward it (and only for that — it is never stored). If you need the relay itself to never see plaintext, this isn't the right tool. We're deliberate about saying so.
And be honest with yourself about what end-to-end would even mean here: the moment you drive a remote machine through an AI tool — Claude, Codex, or any other AI client — that tool is one of the endpoints. It has to read your command output and write the next command in plaintext to do its job, and that plaintext lives on the AI provider's side too. So "no one but me ever sees the text" isn't achievable with this kind of workflow, no matter how the transport is encrypted. If that's a hard requirement, an AI-driven remote shell isn't the right fit.
What happens if my session code leaks?
The code is the credential, so treat it like an SSH private key. To lock everyone out instantly, reset it with change-code — the old code stops working immediately and all account links and blocks are cleared. (You can also block an individual account at any time.) Codes are also stored only in keyed (hashed) form, so a database leak never exposes them.
How do I see who has access to a machine, and remove someone?
From the machine itself: run aicommander-agent list-admins (or open the desktop tray's "Linked Accounts…") to see every account currently linked. Emails are masked (e.g. lu****@wear****.com) — the full address is never sent from the relay. To block one account, use aicommander-agent block-admin <number> (or the Block button in the tray window) — this works at any time, refuses that account's access, and stops it re-linking; the account stays listed in a "Blocked" section so unblock-admin (or the Unblock button) restores it instantly. To lock everyone out at once, reset the code (change-code / "Reset Access Code"), which clears all links and blocks.
I signed in and added a machine but it says "No access" — why?
You connected after the code's first hour. That first hour (after the code is created or reset) is a trusted onboarding window; connect within it and your account is linked with access instantly. After it, the code may have leaked, so for safety a brand-new account is linked but starts blocked until the machine's operator approves it. Ask them to open "Linked Accounts…" on the machine (or run aicommander-agent unblock-admin) and unblock your account — then the "No access" badge clears and it works.
What if my account API key leaks?
API keys are stored hashed and are revocable from the dashboard at any time. By default they're also gated by daily re-activation: a key stays dormant unless the owner has opened the dashboard recently, so a key sitting in a file can't be used on its own indefinitely. A last used timestamp helps you spot unexpected use.
Are any risky features on by default?
No. Higher-reach capabilities are opt-in. Desktop screen sharing is off until you enable it in the tray, and the daily re-activation gate is on by default — you can turn it off, but the dashboard flags that as risk-increasing.
Can an AI agent be tricked into doing something via command output?
We mitigate this: the MCP tools instruct the model to treat all command output strictly as untrusted data to relay back to you, never as instructions to act on. Output is also size-capped so a flood can't destabilize the relay.
Does this open a port on my machine?
No. The agent only makes one outbound WebSocket connection to the relay. Nothing listens for inbound connections, so there's no port to scan, and it works behind NAT and firewalls without changes.
Can other people see my machines or output?
No. Each session is an isolated instance, and a command's output is routed only to the connection that issued it.
Is this a sandbox?
No — it's remote shell access. The agent runs as root by default for administration work; run it as a non-root user to restrict what commands can do. Anyone who knows a current code can run commands on that machine, so the security model is about controlling who holds the code (reset it to lock everyone out, or block an individual account).
Architecture
AI Commander has two parts: the hosted relay (the MCP/REST endpoint your AI client talks to) and a small agent on each machine you control. The agent dials out to the relay, so nothing listens for inbound connections on your machine.
How a command flows
- The agent starts and opens an outbound, encrypted connection to the relay, registering under its session code.
- You mention
AIC-7K3P-WX9M-RTBNto Claude → Claude callsremote_exec("AIC-7K3P-WX9M-RTBN", "df -h"). - The relay authenticates the request and routes it to the isolated session for that code.
- The agent runs the command and streams stdout/stderr back through the relay as SSE events → Claude receives streaming output.
- The process exits → the stream closes with the exit code and a final summary.