Free · self-hosted · phone-first

Your agent terminals.
On your phone.

Drift Assist is a web terminal for the tmux sessions where your coding agents run. Install it on your computer, then open it from your phone.

Your machine. Your sessions. Your network.

SCREENSHOTS + REAL RECORDING · DEMO INSTALL
Demo install on desktop: tmux session tabs above Claude Code in todo-api, asking approval for git push, with the Select option answer bar and ↵, Hist, File, Send and Keys composer controls.

The phone composer

Type into the live terminal pane from your phone. Your agent’s CLI stays on the computer, so you can read its work and send the next prompt from the same screen.

How to use it Open the agent’s tab → type in the composer → Send → Keys → Enter. Read the real Claude response in that pane.

Claude Code · todo-api demo
For the agents you already run

01 / The terminal, within reach

Read, type, and answer.

Your agents keep running in tmux. Assist gives you a way to see and control their panes from a phone browser.

Live terminal panes

Stream pane output, switch sessions, and type into the terminal. Your agent’s CLI stays on the computer.

Run work and switch project tabs · 18.2s · muted

Live sessions and tabs

Read live pane output and switch sessions while your agents keep running in tmux. The terminal work stays on your computer.

How to use it Tap a session tab to open its pane → type into the composer → Send. Tap another tab to switch sessions.

One-tap answers

Permission prompts become tap targets. Optional Auto-Yes has a visible countdown and STOP. It starts off.

Answer a waiting question with one tap · 9s · muted

A question, answered with one tap

Permission prompts become tap targets. You can answer the waiting pane without typing the response into its terminal.

How to use it Read the question → tap its Yes answer. Choose the answer you mean; Auto-Yes starts off.

Notice waiting panes

Sudo and pending-question notices surface panes you are not looking at. Open the pane to see what needs you.

Open a question waiting in another tab · 7.7s · muted

A prompt in another tab

A pending-question notice surfaces a pane you are not looking at. Open that pane to read what needs you; the popup does not answer it for you.

How to use it When Question pending appears, tap Open tab → read the question → tap the answer you choose.

A CLI for your agents

Agents can list, read, and message each other’s panes through the same assist CLI.

All 32 features

From a shell, or from an agent

The assist CLI.

Drive and message tmux panes with ls, view, send, wait, launch, kill, and autoyes.

wait returns the pane’s state: 0 for idle, 10 for a prompt, and 75 when it is still working at the deadline.

Successful ls, view, send, wait and launch commands print next: hints to stderr; send also offers a callback: line with wording to ask the other agent to report back to your pane when it finishes or gets stuck.

The example below reads an existing session named review and sends it a message.

See an agent-to-agent CLI example
Bash · Agent-to-agent example
assist ls
assist view review
assist send review "Check the changes and report back." --enter
Read the CLI reference
List, send, wait and view from the CLI · 27.5s · muted

The assist CLI drives panes

List sessions, send text to a pane and wait for it to settle from a shell. Agents can use the same CLI to message one another’s panes.

How to use it assist ls → assist send todo-api "printf 'Hello from the Assist CLI.\n'" --enter → assist wait todo-api --timeout 8

Your prompt, kept short

What the brackets do.

Give a favourite a handle, then type [handle] to add its full text to your prompt.

Recognised handles appear as green blocks with their expanded character count. Tap the count to preview exactly what the pane will receive.

Assist expands segments on the server when you send. History keeps what you typed, so recalling the prompt brings back the short bracket form.

The [$handle] vault form stores its value in that browser only, in plaintext, as the setup sheet shows.

Recall history and send a saved bracket segment · 14.5s · muted

Stored commands with [handle]

Give a favourite a handle, then type [handle] to add its full text to your prompt. Assist expands it when you send; history keeps the short token, so you can recall it without a screenful of command text.

How to use it Hist → star a command → Favs → ✎ → give it a handle, such as checks. Type [checks] in the composer → Send.

02 / Install

Start on your computer.

Choose the computer where your agents run. No account or Studio subscription is needed.

Choose your computer’s operating system. Showing other Linux.

1 Install the prerequisites

Other Linux

Use your distribution’s package manager

Install Python 3.10 or newer with venv and ensurepip, tmux 3.2 or newer, Git, Bash and curl. Package names vary by distribution. Verify these are present before continuing; read the tmux version printed below.

Bash · Verify your prerequisites
python3 -c 'import sys, venv, ensurepip; assert sys.version_info >= (3, 10)'
tmux -V
git --version
bash --version
curl --version
  • Python ≥ 3.10 + venv
  • tmux ≥ 3.2
  • Git
  • Bash
  • curl for downloads

Install the coding-agent CLIs you want to use separately. Docker and jq are not required.

2 Install Drift Assist

Bash · One-line installer
curl -fsSL https://driftassist.dev/install.sh | bash

The downloadable installer is designed to check out a reviewed source commit, then run the source installer. It needs Bash.

3 Start it, then connect your phone

Start the server

Bash · On your computer
assist start

Opens the local server on your computer. Its default URL is http://localhost:8089/.

Allow your LAN address

Bash · On your computer
assist expose

Also listens on this computer’s private LAN address and allows its browser origin. Restarts the server if it is running.

Pair your phone

Bash · On your computer
assist pair

Prints a URL and QR code. Scan it with your phone on the same trusted LAN. The first device signs in and closes the five-minute window.

On Windows, run all three inside WSL Ubuntu and complete the Windows networking setup above before scanning.

Keep it running after login

Optional: install a systemd user service on Linux or a launchd agent on macOS. It starts now and at login. On WSL, this requires systemd and a running distribution; it does not keep Windows awake.

Bash · Optional service
assist service install
If your shell cannot find assist

For the default install location, add this to your current shell and your ~/.bashrc or ~/.zshrc. If you chose a custom bin directory, use the path printed by the installer.

Bash / zsh · Default install location
export PATH="$HOME/.local/bin:$PATH"
Sign in to your own Assist · 8.8s · muted

Sign in to your own Assist

One shared secret signs you in to your installation. The browser holds a cookie after sign-in, so you only paste the secret once.

How to use it Open Assist → paste your shared secret into the sign-in field → tap Sign in.

Open a project and send a command · 17.5s · muted

Sessions and projects

Your agents keep running in tmux. Switch sessions and type into their terminal panes from the phone browser.

How to use it Open Projects → choose a project/session → type a command → tap Send. Use the session tabs to switch back.

Use File and Keys from the phone composer · 18s · muted

Send, Hist, File and Keys

Send types your prompt into the live pane; Hist returns to what you typed. File and Keys sit beside it, so the file control and keyboard keys stay within reach on a phone.

How to use it Type a command → Send. Tap Hist for history or File for the file control; to cancel the running command shown here, tap Keys → Ctrl+C.

03 / The trust boundary

Your terminal.
Your responsibility.

Read SECURITY.md

One owner, one shared secret

There are no accounts or roles. Anyone holding the secret has the same control of your machine that you do.

Loopback first, trusted LAN when you choose

Flask binds to loopback by default. assist expose adds one private LAN address. Never put this remote shell on the public internet.

Auto-Yes hands over permission

It ships off. Arming it lets an agent proceed after a countdown; STOP cancels an answer. Detection uses pane text, so review the trust implications before enabling it.

Watch the Auto-Yes countdown and stop it · 21s · muted

Auto-Yes: countdown and STOP

Auto-Yes answers permission prompts after a visible countdown. It starts off; arming it hands over permission, and STOP cancels an answer before it goes out.

How to use it Actions → Auto-Yes → choose a delay. During the countdown, tap STOP to keep the answer manual.

04 / Updating

Keep it current.

In-app notice: Update available

Open Actions → Software updates to check or install. From the terminal, check the configured Git upstream, install the update, then restart the web server to use it.

Bash · Check, install, restart
assist update --check
assist update
assist restart

Automatic installation requires a clean checkout on its configured tracking branch. Updates and restarts are separate; your tmux sessions keep running.

05 / Uninstalling

Remove your installation.

Copy your prompt history, favorites, settings, and shared-secret file before deleting the checkout. Removing it deletes the data stored there too.

Show uninstall commands

These commands delete a default installation. Use your actual paths if you changed them. Remove the service first only if you installed it.

Bash · Remove a default installation
assist service uninstall  # only if you installed the service
assist stop
rm ~/.local/bin/assist
rm -rf ~/.config/drift-assist
rm -rf ~/.config/claude-assist  # old config, if present
rm -rf ~/.local/state/drift-assist
rm -rf ~/.local/share/drift-assist

If you opted into the Claude Code status line, restore the installer’s timestamped settings backup or remove its statusLine entry before deleting the checkout.

Optional companion

driftstudio.dev

Drift Studio is the paid design and planning hub. Assist is its free companion and works on its own. Connecting a Studio with a personal token adds its attention inbox and project context to Assist.

Visit Drift Studio ↗

06 / Questions

Before you install.

Does this install my coding agents?

No. Install the agent CLIs on your computer and run them in tmux. Assist streams and types into those panes.

Can I install it on my phone?

No. It runs on a Linux or macOS computer, or inside WSL2 on Windows. Your iPhone or Android phone uses its browser to connect after you pair it.

Can I use it away from my network?

The supported setup is loopback or a trusted LAN you own. Keep Assist off the public internet and public tunnels. Read SECURITY.md for the full boundary.

Does the computer need to stay on?

Yes. Assist and the tmux sessions run on that computer. A sleeping or powered-off host cannot stream terminal output to your phone.

Do I need Drift Studio or an account?

No. Assist is free and self-hosted, with one owner and one shared secret. Studio is an optional connection.

Will it answer every prompt automatically?

Auto-Yes starts off. When you choose to enable it, it detects permission prompts in pane text and answers after a countdown. The all-sessions switch covers every agent pane, including new ones. Review SECURITY.md before arming it.

SECURITY.md

Read-only snapshot from the supplied Assist source. Full document; no network request.

Security posture

Read this before you install Assist. It is not a disclaimer. It is the design.

What Assist is

Assist is a single-owner tool. One person owns the machine, and everyone who can reach Assist is that person. There are no accounts, no roles, no per-user data and no audit trail. There is one shared secret, and holding it means being the owner. If two people hold it, they are the same user as far as Assist is concerned, and neither can see what the other did.

Assist runs arbitrary commands on your machine, by design. That is the product. It types into live tmux panes, starts processes, and /api/commands/run executes command strings, including commands an agent wrote into .assist-commands.json. There is no sandbox and no allowlist, because a terminal with an allowlist is not a terminal. Anything that can reach an authenticated Assist endpoint can do anything your user account can do — read your files, your SSH keys, your cloud credentials, your source code; write anywhere you can write; and reach anything on your network that you can.

Assist must never be exposed to the public internet. Not behind authentication, not behind a VPN you also expose, not "just for a minute". Flask binds 127.0.0.1, plus at most one specific LAN address when you ask for it (assist expose writes ASSIST_BIND; a wildcard such as 0.0.0.0 is refused at start, and assist expose refuses a public address). The supported deployments are exactly two:

Loopback — you use it on the machine it runs on. Trusted LAN — Flask also listens on the host's LAN address, or a reverse proxy on your own network forwards to loopback, and every device that can route to it is a device you own.

If you port-forward it, put it on a public cloud host, or expose it through a tunnel, you have published a remote shell on your machine with a single password and no rate limit. Nothing in this repository will save you.

Assist has not been reviewed for multi-tenant use and is not built for it. Do not deploy it for a team.

The trust boundary, in one paragraph

The only thing protecting Assist is who can reach the port and who holds the secret. Input validation is not a control here, because the input is meant to be arbitrary. So the security of your install is entirely the security of your network and of auth_token.

The shared secret

auth_token is generated on first start, written beside the code with mode 0600, and gitignored. A direct start attached to a terminal prints the value and its file path. Redirected startup output prints only the path, so the raw value is not copied into the server log. It is presented one of two ways and no others:

* the browser exchanges it once at /login for a cookie holding an HMAC of it — the raw secret is never stored client-side; * scripts and the assist CLI send it in the **X-Assist-Token header**.

The browser login field uses autocomplete="current-password", so a browser password manager may offer to store this shared secret.

**A ?token= query parameter is not accepted.** A credential in a URL gets written down by things Assist does not control: the reverse proxy logs the full request line, the browser keeps it in history and in any bookmark or shared link, and it leaves in the Referer header of the next outbound request. Use the header. (For a headless browser, set it on the browser context — a token in the URL would authenticate the HTML document but none of its stylesheets or scripts anyway.)

The login cookie lasts ten years and is never revalidated. That is deliberate — a phone must not be asked to re-authenticate mid-session — and it means **revocation is deleting auth_token and restarting**. Every issued cookie stops matching, because the HMAC key changed. Do that if a device is lost, if the token was pasted somewhere it should not have been, or if you are not sure.

Three endpoints are exempt from the gate: /login, /health (a liveness probe whose complete response body is {"status":"ok"}), and /api/cli-proxy. Containers cannot hold the token, so the proxy is fenced at the proxy layer to the container subnet and stays fail-closed on its own ASSIST_CLI_ALLOWED allowlist — empty means disabled. It is also currently stopped by the temporary execution park before any subprocess runs. The device-approval pair is also reachable without a token, because a device with no token is exactly who calls it; it is fenced instead by a LAN allowlist, a cap on pending requests, a per-IP cooldown, and a single-use claim that binds an approval to the browser that asked.

Temporary execution park

While container host wiring migrates, Assist refuses only these eight intents: automate_start, automate_hard_relaunch, automate_soft_clear, automate_soft_resend, automate_trust_answer, automate_auto_answer, configured_cli_proxy, and configured_image_build. That parks Automate's launch/relaunch/clear/resend/trust/answer execution, /api/cli-proxy, and /api/container/build. Saved commands, fixed git operations, project-venv creation, configured restart, terminal init/duplicate/run-init, and the native folder picker remain available.

There is no API or CLI operation that un-parks an intent, and changing the published park phase does not change this deny-list. A refused HTTP operation returns 409; for Automate start the exact body is:

json
{
  "ok": false,
  "error": "container_launch_parked",
  "reason": "Container launch automation is temporarily parked while host wiring migrates.",
  "intent": "automate_start"
}

Launch provenance on a fresh install

Ordinary startup automatically initializes a missing .assist-launch-provenance-v1/ store as an empty epoch and writes the .assist-launch-provenance-v1-initialization.json receipt. Initialization runs only when the store path does not exist. An existing corrupt or incomplete store is never repaired or replaced at startup; readers continue to fail closed. The park-handoff startup path does not initialize provenance.

Auto-Yes: what arming it means

Auto-Yes watches tmux panes for permission prompts and answers them for you after a countdown. When it is armed, an agent no longer has to ask you before doing something you would have been asked about — running a command, writing a file, deleting one. That is the point of the feature and it is a real transfer of authority, not a UI convenience.

Two things follow, and you should decide about both before turning it on.

It answers by pattern-matching pane text, and pane text is not trustworthy. An agent that prints a diff, cats a README, or renders something it fetched has put text on the screen that Assist did not author. The detectors are anchored so that ordinary output does not trip them — a bare (y/n/a) must end its line, option rows must have the shape of a rendered button row, numbered menus must sit under a live footer with no internal divider — and tests/test_autoyes_detection.py pins both directions. Those anchors are narrow enough for real code and prose to pass under them, and they are not proof against text written specifically to trip them. If you run agents over content from sources you do not control, that is the risk you are accepting.

The all-sessions switch is much larger than it looks. autoyes.all_sessions arms every agent pane on the host — claude, codex, opencode, cursor, gemini — at one delay, including sessions created after you flipped it. Plain shell panes stay manual, which is what keeps apt, ssh host-key and stray (y/n) prompts out of scope, and a session turned off by hand records an opt-out that survives a restart.

A session armed by hand follows the same shell rule. Whatever runs in a shell pane decides what it prints, so curl … | sh or cat of a hostile file can draw a (y/n) line. Shell panes are answered only in a session armed by hand whose project settings say autoyes.shell_ok: true (Shell panes, in the Auto-Yes delay picker); it ships off, and the all-sessions switch ignores it.

Shipped defaults, which a fresh clone gets because settings.json is gitignored:

SettingShips asMeans
autoyes.all_sessions"off"nothing is armed until you arm it, per session
autoyes.default_delay5five seconds to see the prompt and cancel

The countdown is the only chance you get to stop an answer, so it is clamped to at least 100 ms even if a settings file says 0. A sub-second delay is effectively no window at all. Every start prints the current posture to the log next to the auth-token file notice, so an armed install can never be a silent one:

Source output
[assist] auto-yes: ALL SESSIONS ARMED at 0.1s — every agent pane on this host …

Check it with assist autoyes --global --status, or per session with assist autoyes <session> --status, which tells you which rule applied.

Secret vault: plaintext in this browser

Assist can store values for [$handle] tokens in the browser's localStorage under assist.vault.v1. Those values are plaintext. They are not encoded, encrypted or sent to the server for storage. When a token resolves, the browser sends the resulting value only as /type content with secret: true, which turns off trimming, first-word case fixing, segment expansion and history.

This buys one specific boundary: someone who steals only an Assist auth cookie on another device cannot retrieve the vault and does not get a stored path to root. It does not protect against someone holding the unlocked browser or reading that browser profile. That device already controls the terminal.

Script execution in Assist's origin is a separate boundary, not another way of saying “holding the browser.” Injected or compromised same-origin JavaScript can read the plaintext vault without possessing the device, then send or disclose those values using the browser's authenticated session. Preventing untrusted script from running in this origin is therefore part of protecting the vault.

localStorage is scoped to the exact origin: scheme, host and port. Moving an install from http://assist.example.lan to https://assist.example.lan creates a different store. The existing vault does not migrate and will appear empty on HTTPS; any needed entries must be entered again on the new origin, then forgotten from the old origin while it is still reachable.

Encryption waits on trusted TLS for assist.example.lan. The phone reaches Assist over plain HTTP, which is not a secure context; browsers therefore do not make crypto.subtle available there. Service workers have the same secure-context requirement (apart from browsers' loopback exception), so the offline shell is also inert on a phone using the current plain-HTTP LAN origin. Once TLS lands, the intended design is a WebAuthn-gated vault whose key stays behind the platform authenticator. Assist will not hand-roll AES or PBKDF2 in JavaScript to route around the secure-context requirement.

Use the per-entry Forget controls or Forget all vault entries in Settings to remove browser-stored values.

The [$sudo] quick key is offered from prompt shape alone. A pane can fake that shape, so look at the prompt before you tap it.

The sudo popup asks for more than shape. It appears only when sudo's prompt is a pane's last line, exactly one childless process is waiting in that pane's foreground and it is sudo, and the terminal has echo switched off. The single waiter is what rules out a cmd | sudo -S pipeline, where sudo reads the pipe and a value typed into the pane would reach the sibling instead: that has two waiters. Whether sudo reads the terminal or a pipe cannot be observed directly — sudo is setuid-root, so its file descriptors are not readable here, and echo is a shared terminal switch — so the popup errs toward Open tab. The command it shows is sudo's own argv, not pane text. Its Send names that process, and the server types nothing unless the same test still holds for that pid: a sudo that runs its command without forking keeps its pid, which may belong to ssh by then.

Question popups answer nothing. They say which tab has a prompt waiting and offer to open it. Their card is keyed by a fingerprint of the last 40 lines of the prompt on screen; a prompt taller than that (a very long command wrapped at a narrow width) can re-key when the pane scrolls, so a dismissed card may reappear once. It is a re-notification, never an answer.

What is deliberately not here

No server-stored sudo password. Assist used to keep one in sudo_pw.dat and type it into a pane on request. It is gone. A borrowed auth cookie must not be enough to ask the server for root without ever seeing the password. You can type a password directly — the composer detects the live prompt and sends it byte-for-byte without history — or opt into the browser-local plaintext vault described above. The server has no endpoint that stores or returns either value.

If you have an old install, delete the leftover file:

bash
rm -f sudo_pw.dat

No secret redaction. Terminal output is streamed verbatim. If a command prints a key, Assist shows it and stores it in the session capture.

**No rate limiting on /login.** The token is 32 bytes from secrets; the control is its entropy, not attempt throttling. This is another reason the port must not be public.

Hardening checklist

1. Keep Flask on 127.0.0.1 plus, at most, the one LAN address assist expose sets — or put a reverse proxy on the LAN address instead. Never a wildcard. 2. Set ASSIST_ALLOWED_ORIGINS to the hostnames you actually use. It ships loopback-only, so a LAN install that skips this gets 403 on every POST. 3. Narrow access.open_networks from the shipped all-private-ranges default to the one range your devices are on. 4. Leave ASSIST_CLI_ALLOWED empty unless you use containers. Empty disables /api/cli-proxy entirely. 5. Leave Auto-Yes off until you have watched it answer a prompt you were expecting, and leave the all-sessions switch off unless you actually want every pane on the machine covered.

Reporting

This is a single-maintainer hobby project with no security SLA. Open an issue for anything that is not itself sensitive; for something that is, contact the maintainer privately rather than filing publicly. Expect a best-effort response and no coordinated-disclosure process.

MIT licence

MIT License

Copyright (c) 2026 Drift Further LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Copy manually

Your browser did not allow clipboard access. The text below is selected; use your device’s copy command.