wingthing runs AI agents sandboxed on your machine, accessible from anywhere.
Your machine is now a wing — a remote-accessible sandbox host. Open a terminal from the browser, pick an agent (Claude, Codex, Ollama), and it runs in an isolated egg on your hardware. All traffic is end-to-end encrypted through the roost.
wingthing is a CLI tool (wt) that lets you run Claude Code, Codex, Ollama, and other AI agents in sandboxed sessions on your own hardware - then access them from any device through an encrypted connection.
| term | what it is |
|---|---|
| wing | Your machine. Install wt, run wt start, and it connects outbound to the roost. One machine = one wing. |
| roost | The server. Handles login and routes connections. wingthing.ai is a roost. wt roost start runs your own. |
| egg | A sandboxed agent session. Each egg is an isolated process with its own PTY, sandbox profile, and gRPC socket. Runs Claude, Codex, Ollama, etc. |
Wings connect outbound to the roost (no port forwarding needed), and the roost bridges WebSocket connections between wings and browsers. All PTY traffic is E2E encrypted.
Single binary, no dependencies. macOS and Linux, x64 and arm64.
Installs to ~/.local/bin/wt. Set WT_INSTALL_DIR to change the location.
Or grab the binary from GitHub releases.
To update:
Three commands to go from install to remote access:
wt start runs the wing as a background daemon. Use wt wing start --foreground for debugging.
Other wing commands:
Wing settings live in ~/.wingthing/wing.yaml. You can edit the file directly or use the CLI:
The daemon picks up changes via SIGHUP. See the reload column for what requires a restart.
| field | effect | reload |
|---|---|---|
| org | organization slug - share this wing with org members | restart |
| paths | project directories to expose (default: CWD). Also settable via wt start --paths ~/repos,~/projects | live |
| labels | freeform tags shown in the web UI (e.g. "gpu", "linux", "work") | live |
| locked | enable passkey access control. See access control | live |
| auth_ttl | passkey nonce lifetime. Default 0 (boot-scoped - valid until wing restarts). Set to e.g. 1h to force re-authentication periodically | live |
| admins | emails with admin role - admins see all sessions and all paths | live |
| idle_timeout | kill idle sessions after this duration (e.g. 4h). Default: disabled | live |
| egg_config | path to default egg.yaml for sessions on this wing | live |
| audit | record session terminal output for replay | live |
| debug | verbose daemon logging | live |
| conv | conversation context file path | live |
By default, anyone with your account credentials can start sessions on your wing. Locking it adds a second factor using passkeys - the same WebAuthn standard behind "Sign in with Face ID" and "Sign in with Windows Hello."
A passkey is a cryptographic credential stored in your password manager (1Password, LastPass, iCloud Keychain, Bitwarden) or your OS (Touch ID, Windows Hello, Android biometrics). Most people already have one and don't know it.
When your wing is locked, browsers must prove they hold a passkey on the allowlist before the wing will start or reattach a session. The roost can't fake this - the cryptographic challenge-response happens directly between your browser and your wing.
Once locked, the browser shows an "authenticate with passkey" button when you open a session. Click it, verify with your fingerprint or password manager, and the session starts. The wing issues a nonce that's valid for the rest of the boot - you won't be prompted again until the wing restarts. Set auth_ttl if you want periodic re-authentication on top of that.
When locked, only passkeys on the allowlist can access the wing:
Removes the passkey requirement. Sessions start without a second factor.
The allowlist lives in wing.yaml on your machine. The roost stores your passkey's public key and credential ID so it can hand them to your wing during wt wing allow. The security model assumes the roost is not compromised at the time you add a key - after that, verification happens entirely on the wing.
Orgs let a team share wings. Create one from the web UI (account page), then invite members by email. Members see all wings in the org.
To attach a wing to an org:
Or pass it at start time:
Org wings show up for all org members in the web UI. Combine with wt wing lock to require passkey auth - org membership gets you visibility, but the wing owner controls who can actually start sessions.
Kill sessions that go quiet. Set idle_timeout in wing.yaml:
A session is idle when there's no output from the agent and no input from the user. Agent thinking generates PTY output, so active sessions won't be killed. Default is disabled - sessions run until stopped manually.
Without an egg.yaml, the sandbox applies opinionated defaults: CWD is writable, home is read-only, sensitive directories (~/.ssh, ~/.gnupg, ~/.aws, etc.) are denied, and only essential env vars pass through. ~/.ssh/known_hosts is preserved read-only so SSH can verify host keys without prompting (add deny:~/.ssh/known_hosts to disable this). Network is blocked except for exactly what your agent needs - each agent has a profile declaring its API domains, env vars, and config directories. Claude gets api.anthropic.com, Ollama gets localhost, etc. You don't configure any of this.
A local CONNECT proxy enforces domain-level filtering - agents can only reach their own API, not the entire internet. On macOS, Seatbelt forces all traffic through the proxy at the OS level. On Linux, the proxy is set via HTTPS_PROXY.
Drop an egg.yaml in your project to customize. Configs are additive - you only declare what you're changing from the defaults.
Place egg.yaml in your project directory for per-project config, or in ~/.wingthing/egg.yaml for global defaults. Project configs are discovered automatically. Use the sandbox builder on the homepage to generate configs visually, or install the Claude Code skill for an interactive security interview that generates hardened configs.
Example egg.yaml:
The sandbox auto-injects mounts for the agent binary's install root and config directory, so you don't need to know where Claude or Codex is installed. Use base: none for a blank slate if you want full control.
| field | effect |
|---|---|
| fs | filesystem rules: rw:PATH, ro:PATH, deny:PATH, deny-write:PATH. Additive on top of defaults |
| network | domain allowlist, merged with agent defaults. Use "*" for unrestricted |
| env | extra env vars to pass through (agent-required vars are auto-injected) |
| dangerously_skip_permissions | let the agent run without asking - the sandbox is the permission boundary |
| agent_settings | map of agent name to settings file path (e.g. claude: /path/to/settings.json). Host settings override user prefs |
| base | inherit from another config. none for blank slate, or a path to another egg.yaml |
| cpu * | CPU time limit as a duration string, e.g. 300s or 5m. Linux only, ignored on macOS |
| memory * | memory limit, e.g. 4G. Linux only, ignored on macOS |
| max_fds * | max open file descriptors. Linux only, ignored on macOS |
| max_pids * | max processes in cgroup. Linux only, requires cgroups v2 |
* Linux only. macOS Seatbelt doesn't support resource limits. If these limitations are blocking you, file an issue.
Run your own roost. Single binary, SQLite, no external dependencies.
Open localhost:8080. Server and wing in one process, no login page, no config.
Same command, add OAuth env vars. Now multiple people can log in.
Or use Google OAuth with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. Connect additional wings from other machines with wt login --roost https://my-roost.example.com and wt start --roost https://my-roost.example.com. Or set roost_url in ~/.wingthing/config.yaml to avoid passing the flag every time.
If you need the server and wing on different machines, run them as separate processes:
| var | purpose |
|---|---|
| WT_BASE_URL | public URL for OAuth callbacks |
| GITHUB_CLIENT_ID | GitHub OAuth app ID |
| GITHUB_CLIENT_SECRET | GitHub OAuth app secret |
| GOOGLE_CLIENT_ID | Google OAuth client ID |
| GOOGLE_CLIENT_SECRET | Google OAuth client secret |
| OPENAI_API_KEY | embeddings (optional) |
| WT_JWT_KEY | EC P-256 private key (PEM or base64-DER) for JWT signing (required in server mode, auto-generated in local/roost mode) |
| WT_APP_HOST | hostname for app subdomain (e.g. app.example.com) |
| WT_WS_HOST | hostname for WebSocket subdomain (e.g. ws.example.com) |
Without OAuth env vars, the server auto-enables local mode (single-user, no login page). Pass --local explicitly to force it.
The roost is a dumb pipe. It forwards encrypted blobs between browsers and wings without reading them. All session data, terminal output, file listings, and audit recordings are end-to-end encrypted. The roost knows wing IDs and connection state - nothing else.
Your wing opens an outbound WebSocket to the roost and holds it open. When you open a terminal in the browser, the browser connects to the roost over a second WebSocket. The roost matches them by wing ID and forwards messages both ways. All traffic is encrypted end-to-end.
Every connection between your browser and your wing gets a fresh X25519 key pair on both sides. The two do a Diffie-Hellman exchange, derive an AES-GCM key via HKDF, and encrypt everything from that point. Terminal I/O, file listings, session history, audit data - all ciphertext as far as the roost is concerned. Keys live in sessionStorage and vanish when you close the tab. Previous sessions can't be decrypted, even by the wing.
Encryption keeps the roost from reading your data. Passkeys control who can start sessions in the first place. When your wing is locked, the browser sends a WebAuthn assertion directly to the wing inside the encrypted channel. The roost forwards the blob but can't read or forge it. The wing verifies the signature against its local allowlist and issues a boot-scoped nonce for subsequent requests.
User accounts, OAuth tokens, org membership, device auth tokens, bandwidth counters. That's it. No terminal data, no file contents, no session recordings. If you self-host, the SQLite database contains only auth state.
Three security domains, each solving a different problem.
Agents run inside a platform-native sandbox - Seatbelt on macOS, user namespaces + seccomp on Linux. The sandbox controls filesystem access, network reach, and system calls. Configure it with egg.yaml; see egg configuration.
All traffic between your browser and your wing is E2E encrypted (X25519 + AES-GCM). The roost forwards ciphertext and can't read it. Wings connect outbound only - no inbound ports, no static IP, works behind any NAT or firewall. Lock your wing and sessions require a passkey on top of encryption - the roost can't start sessions on your behalf even if it wanted to.
The roost handles login (OAuth, device auth) and routes connections to the right wing. When a wing is locked, the roost stores passkey credential IDs so it can hand them to the wing during wt wing allow - but the WebAuthn verification happens on the wing, not the roost. See access control.
| feature | macOS | Linux |
|---|---|---|
| deny paths (.ssh, .aws, etc.) | SBPL rules | tmpfs overlays |
| write isolation (HOME read-only) | SBPL rules | bind-mount read-only + writable holes |
| network deny | full block via SBPL | network namespace (CLONE_NEWNET) |
| domain filtering | SBPL forces traffic through local CONNECT proxy; only whitelisted domains pass | CONNECT proxy via HTTPS_PROXY (cooperative) |
| seccomp syscall filter | n/a | blocks mount, ptrace, kexec, setns, unshare, bpf, open_by_handle_at, and 20+ more |
| resource limits (CPU, mem, FDs) | not available | cgroups v2 (memory, PIDs) + rlimit(2) (CPU, virtual address space, FDs) |
| PID isolation | n/a | CLONE_NEWPID |
* See sandbox limits below.
The sandbox is not a container runtime. Known gaps:
HTTPS_PROXY. Agent harnesses that respect proxy env vars (Claude Code, Codex, most Node.js and Go tooling) route through it automatically. An agent that spawns raw sockets can skip it. On macOS, Seatbelt forces all traffic through the proxy at the OS level. If your agent's harness doesn't respect HTTPS_PROXY, file an issue."deny:~/.secrets" to the fs list to hide sensitive directories.~/.claude/ and similar directories must be writable for the agent to function.These are on the roadmap. If any of these are blocking you, file an issue - it helps us prioritize. If you need hard isolation today, run wingthing inside a VM or container.