documentation

wingthing runs AI agents sandboxed on your machine, accessible from anywhere.

quickstart

using wingthing.ai

$ curl -fsSL https://wingthing.ai/install.sh | sh
$ wt login
$ wt start
$ open app.wingthing.ai

run your own

$ curl -fsSL https://wingthing.ai/install.sh | sh
$ wt roost start # server + wing, one command
$ open localhost:8080

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.

overview

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.

termwhat it is
wingYour machine. Install wt, run wt start, and it connects outbound to the roost. One machine = one wing.
roostThe server. Handles login and routes connections. wingthing.ai is a roost. wt roost start runs your own.
eggA 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.

installation

Single binary, no dependencies. macOS and Linux, x64 and arm64.

$ curl -fsSL https://wingthing.ai/install.sh | sh

Installs to ~/.local/bin/wt. Set WT_INSTALL_DIR to change the location.

Or grab the binary from GitHub releases.

To update:

$ wt update

wing setup

Three commands to go from install to remote access:

$ wt login # authenticate with the roost
$ wt start # start the wing daemon
$ open app.wingthing.ai # start terminals from your browser

wt start runs the wing as a background daemon. Use wt wing start --foreground for debugging.

Other wing commands:

$ wt wing status # check daemon status + active sessions
$ wt stop # stop the wing daemon

wing configuration

Wing settings live in ~/.wingthing/wing.yaml. You can edit the file directly or use the CLI:

$ wt wing config # view current config
$ wt wing config set org=myteam # set a value
$ wt wing config set audit=true # enable session audit recording

The daemon picks up changes via SIGHUP. See the reload column for what requires a restart.

config fields

fieldeffectreload
orgorganization slug - share this wing with org membersrestart
pathsproject directories to expose (default: CWD). Also settable via wt start --paths ~/repos,~/projectslive
labelsfreeform tags shown in the web UI (e.g. "gpu", "linux", "work")live
lockedenable passkey access control. See access controllive
auth_ttlpasskey nonce lifetime. Default 0 (boot-scoped - valid until wing restarts). Set to e.g. 1h to force re-authentication periodicallylive
adminsemails with admin role - admins see all sessions and all pathslive
idle_timeoutkill idle sessions after this duration (e.g. 4h). Default: disabledlive
egg_configpath to default egg.yaml for sessions on this winglive
auditrecord session terminal output for replaylive
debugverbose daemon logginglive
convconversation context file pathlive

access control

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.

lock your wing

$ wt wing lock

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.

manage the allowlist

When locked, only passkeys on the allowlist can access the wing:

$ wt wing allow # list allowed users
$ wt wing allow --email [email protected] # allow a specific user
$ wt wing allow --all # allow all org members
$ wt wing revoke [email protected] # remove from allowlist
$ wt wing revoke --all # clear the allowlist

unlock

$ wt wing unlock

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.

organizations

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:

$ wt wing config set org=myteam

Or pass it at start time:

$ wt start --org myteam

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.

shared wings

Multiple users can share a single wing. Each user gets an isolated home directory, and you can restrict which folders each person sees.

adding users

Set up OAuth on your roost (GitHub or Google), and users log in through the browser. Each authenticated user automatically gets their own home directory at ~/.wingthing/user-homes/<hash>/ with shell configs and agent binaries symlinked from the host. The owner gets a per-user home too - there's no bypass for local users.

restricting paths

By default, all users see all configured paths. Add members to restrict access per folder:

# wing.yaml
paths:
  - ~/docs                                    # everyone
  - path: ~/repos/api
    members: [[email protected], [email protected]]     # only alice and bob
  - path: ~/repos/infra
    members:
      - [email protected]                        # only carol

Owners and admins always see everything regardless of member lists.

admin role

Add emails to the admins list in wing.yaml. Admins see all sessions and all paths - same visibility as the wing owner.

# wing.yaml
admins:
  - [email protected]
  - [email protected]

agent settings

Override agent config for all users on a wing with agent_settings in egg.yaml:

# egg.yaml
agent_settings:
  claude: /opt/wingthing/claude-settings.json

Host settings take precedence over user preferences. For Claude, apiKeyHelper in the settings file keeps ANTHROPIC_API_KEY out of the agent's environment.

environment

Every egg gets these variables, available to CLAUDE.md, scripts, and any process inside the session:

variablevalue
WT_SESSION_IDsession UUID
WT_USERnormalized username from email
WT_USER_EMAILauthenticated email address
WT_USER_NAMEdisplay name (Google full name, GitHub login)
WT_PREVIEW_DIRworking directory path
WT_PREVIEW_FILE.wt-preview-<session-id>

idle timeouts

Kill sessions that go quiet. Set idle_timeout in wing.yaml:

# wing.yaml
idle_timeout: 4h

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.

egg configuration

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:

fs:
  - "ro:~/.ssh"         # override the default deny for ~/.ssh
network:
  - "github.com"       # add a domain on top of agent defaults
env:
  - SSH_AUTH_SOCK       # pass SSH agent socket
dangerously_skip_permissions: true

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.

what each config field does

fieldeffect
fsfilesystem rules: rw:PATH, ro:PATH, deny:PATH, deny-write:PATH. Additive on top of defaults
networkdomain allowlist, merged with agent defaults. Use "*" for unrestricted
envextra env vars to pass through (agent-required vars are auto-injected)
dangerously_skip_permissionslet the agent run without asking - the sandbox is the permission boundary
agent_settingsmap of agent name to settings file path (e.g. claude: /path/to/settings.json). Host settings override user prefs
baseinherit 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.

self-hosting

Run your own roost. Single binary, SQLite, no external dependencies.

quick start

$ wt roost start

Open localhost:8080. Server and wing in one process, no login page, no config.

multi-user

Same command, add OAuth env vars. Now multiple people can log in.

$ GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy wt roost start

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.

running separately

If you need the server and wing on different machines, run them as separate processes:

$ wt serve # server only
# on another machine:
$ wt start --roost http://host:port # wing only

systemd

[Unit]
Description=wingthing roost
After=network.target

[Service]
ExecStart=/usr/local/bin/wt roost start --foreground
Restart=always

[Install]
WantedBy=multi-user.target

docker

$ docker build -t wingthing .
$ docker run -p 8080:8080 -v wt-data:/data wingthing

fly.io

$ fly launch --name my-wingthing
$ fly deploy

roost configuration

varpurpose
WT_BASE_URLpublic URL for OAuth callbacks
GITHUB_CLIENT_IDGitHub OAuth app ID
GITHUB_CLIENT_SECRETGitHub OAuth app secret
GOOGLE_CLIENT_IDGoogle OAuth client ID
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
OPENAI_API_KEYembeddings (optional)
WT_JWT_KEYEC P-256 private key (PEM or base64-DER) for JWT signing (required in server mode, auto-generated in local/roost mode)
WT_APP_HOSThostname for app subdomain (e.g. app.example.com)
WT_WS_HOSThostname 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.

architecture

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.

how the pieces connect

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.

encryption

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.

passkey auth

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.

what the roost stores

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.

security

Three security domains, each solving a different problem.

the egg: protecting you from the agent

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.

the wing: protecting you from the roost

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: controlling access

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.

what the sandbox enforces

featuremacOSLinux
deny paths (.ssh, .aws, etc.)SBPL rulestmpfs overlays
write isolation (HOME read-only)SBPL rulesbind-mount read-only + writable holes
network denyfull block via SBPLnetwork namespace (CLONE_NEWNET)
domain filteringSBPL forces traffic through local CONNECT proxy; only whitelisted domains passCONNECT proxy via HTTPS_PROXY (cooperative)
seccomp syscall filtern/ablocks mount, ptrace, kexec, setns, unshare, bpf, open_by_handle_at, and 20+ more
resource limits (CPU, mem, FDs)not availablecgroups v2 (memory, PIDs) + rlimit(2) (CPU, virtual address space, FDs)
PID isolationn/aCLONE_NEWPID

* See sandbox limits below.

sandbox limits

The sandbox is not a container runtime. Known gaps:

  • Linux domain filtering is cooperative. The CONNECT proxy is set via 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.
  • Agent credentials are readable. API keys and config files are mounted into the sandbox. The agent can read them, but domain filtering means the only place it can send them is the agent's own API.
  • HOME is readable. The agent reads your codebase. Add "deny:~/.secrets" to the fs list to hide sensitive directories.
  • Agent config is writable. ~/.claude/ and similar directories must be writable for the agent to function.
  • Resource limits are Linux-only. macOS agents can consume unbounded CPU and memory. On Linux, cgroups v2 limits real memory (RSS) and process count; rlimit covers CPU time, virtual address space, and file descriptors. Cgroups require v2 with delegation. Falls back to rlimit-only when unavailable.

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.