Prthole

The Porthole manual

How to set up the daemon, pair a phone, and use everything the app does after that. The phone and the computer on this page are drawn to match the real app and Claude Code, and they step through the real flows; Previous and Next under each one go a step at a time.

What Porthole is

Two pieces. portholed is a small daemon on the Mac or Linux computer where Claude Code runs. Porthole is the Android app. They talk over your tailnet only: the daemon binds the computer's Tailscale addresses and nothing else, identifies the phone through Tailscale's own WhoIs, and admits a new phone only with a pairing code you read off the computer's screen. There is no account and no Porthole server; your sessions travel only between your phone and your computer.

What the phone gets: every running session with what it is doing, a feed of the conversation and its tool calls, Claude's questions as tappable choices, approvals, a real terminal, the git diff of what changed, notifications when something finishes or needs you, and the ability to start or resume sessions, open a dev server, and install builds of the Android apps you are working on.

Porthole  ── WebSocket, port 8737 ──►  portholed ──►  tmux ──►  claude
(phone)                  your tailnet    ├ reads the transcripts
                                         ├ receives the approval hook
                                         └ bridges the tmux pane
          ── SSH ────────────────────►  a shell, when portholed is down
             (Tailscale SSH, or on a Mac the phone's own key)

Session state comes from three places, each doing what it is best at: Claude Code's transcript (the history), a Claude Code hook (approval requests), and the tmux pane (the terminal, and what is on screen right now).

Being plain about what this is. A phone paired with Porthole can act as you on that computer: run commands, read and write files, answer and approve. That is the product, not a side effect. Pair your own phone, and revoke it if you lose the device.

What it needs

  • A Mac with macOS 13 or later, or a Linux computer with systemd user services. Either needs tmux, running Claude Code inside tmux - porthole starts it there for you. tmux is where the phone types; a Claude Code outside tmux can be watched but not sent prompts.
  • Tailscale on the computer and on the phone, signed into the same tailnet. On a Mac, the Tailscale app from the App Store or Tailscale's website.
  • An Android phone with Android 10 or newer.
  • portholed on the computer, from the steps below: with Homebrew, or the installer, which builds it with Go or uses the release binary for macOS or Linux, Intel or ARM.

There is no iOS app, and the daemon does not run on Windows.

Getting started

Once per computer: Tailscale on both devices, the app on the phone, the daemon on the computer, then pairing. The app's first run walks through the same steps for the computer you choose, and every command it shows can be copied.

The steps differ between a Mac and Linux. Pick yours; every step on this page follows the choice.

  1. Tailscale on both devices

    On the phone. Install Tailscale and sign in. Porthole does not embed Tailscale; the official app's VPN already routes the tailnet.

    On a Mac

    On the Mac. Install the Tailscale app from the Mac App Store or tailscale.com, and sign in to the same tailnet.

    Then turn on Remote Login in System Settings › General › Sharing. The Tailscale app on a Mac has no SSH server of its own, so Remote Login is how the phone gets back in if portholed ever stops answering. Once the phone is paired, it adds its own key for it (see The failsafe).

    On Linux

    On the computer. Install Tailscale, then sign in with Tailscale SSH on:

    sudo tailscale up --ssh

    Already signed in? sudo tailscale set --ssh. Tailscale SSH is what the phone falls back to if portholed ever stops answering (see The failsafe). Your tailnet's access rules must also allow SSH to this computer.

  2. Install the app

    On the phone. Download porthole.apk from the latest GitHub release and open it. Android asks once to allow installs from your browser. After that, Porthole tells you about new releases itself (see Updates).

    Open Porthole and tap Get started. What you'll need lists the three things on your side: Tailscale, Claude Code inside tmux, and portholed. There is no account to create.

  3. Install the daemon

    On a Mac

    On the Mac, with Homebrew. It installs portholed and tmux; portholed setup then starts the background service and adds the approval hook to Claude Code:

    brew install shrimpscript/tap/porthole
    portholed setup

    The service is a launchd agent: it starts when you log in and restarts if it fails. After an unattended reboot it waits until someone logs in, unless the Mac logs in by itself (System Settings › Users & Groups › Automatically log in). While a session works or a phone is connected, it keeps the Mac from sleeping, on the power adapter only.

    portholed setup also has the daemon look into your Desktop, Documents and Downloads folders, so macOS asks now, while you are at the Mac, whether portholed may use them. Allow it: a Claude Code started from the phone works in those folders as portholed, and a question nobody is there to answer leaves it with "Operation not permitted". Likewise, the first screenshot taken from the phone makes macOS ask to let portholed record the screen.

    No Homebrew? Clone the repository and run ./tools/install.sh, as on Linux.

    On Linux

    On the computer. You need tmux from your package manager (sudo apt install tmux, sudo dnf install tmux, sudo pacman -S tmux). Clone the repository, read the installer, see what it would do, then run it:

    git clone https://github.com/ShrimpScript/porthole
    cd porthole
    ./tools/install.sh --dry-run
    ./tools/install.sh

    Then keep the daemon running after you log out, which is exactly when you are away:

    sudo loginctl enable-linger $USER

    The service is a systemd user service, portholed.service, started at login and restarted if it fails.

    Homebrew works on Linux too: brew install shrimpscript/tap/porthole, then portholed setup.

    Neither is a curl | sh one-liner, deliberately: both install a background service that can run commands as you, and a hook in your Claude Code settings.

    On the phone. Set up the computer shows the same steps for the computer you chose, each command copyable.

  4. Start Claude Code with porthole

    On the computer, in your project's folder, run porthole where you would run claude. It starts Claude Code inside tmux, in a session named after the folder, so the phone can see its terminal and type to it. Running it again in the same folder brings that session back instead of starting a second one, and arguments go straight to Claude Code (porthole --resume). Already working inside tmux? Plain claude works there too.

    porthole
  5. Check the computer

    On the computer. portholed doctor checks everything the phone will depend on, one line each, in the order you would fix them:

    portholed doctor

    On a Mac

    ok    tmux: tmux 3.5a
    ok    tailscale: running as studio.tailnet.ts.net (192.0.2.20)
    ok    key expiry: this computer: disabled, it will not expire
    ok    ssh failsafe: Remote Login is on and 1 phone key(s) can sign in, from their tailnet addresses only
    ok    after a reboot: the Mac logs in by itself, so the daemon starts without you
    info  sleep: the Mac sleeps after 10 idle minutes; portholed keeps it awake while a session works or a phone is connected, on the power adapter
    ok    disk: 188 GB free of 460 GB where transcripts and state live
    ok    claude: 2.1.270 (Claude Code)
    ok    sessions: 2 running now, all in tmux (registry ~/.claude/sessions)
    ok    transcripts: 9 project directories under ~/.claude/projects
    ok    changes: git version 2.50.1 (what changed, on the phone)
    ok    capture: screencapture, built in (ffmpeg shrinks clips); macOS asks at the Mac for Screen Recording permission the first time the phone takes one
    ok    daemon: portholed 0.28.0 answering, 1 paired device(s), 1 connected now
    ok    service: launchd agent dev.shrimpscript.portholed running, starts at login
    ok    approvals: permission hook registered in ~/.claude/settings.json
    info  builds: 0 published in ~/.config/porthole/builds

    The daemon's log: tail -f ~/Library/Logs/portholed.log

    On Linux

    ok    tmux: tmux 3.5a
    ok    tailscale: running as workstation.tailnet.ts.net (192.0.2.10)
    ok    key expiry: this computer: disabled, it will not expire
    ok    ssh failsafe: Tailscale SSH is on; the phone can open a shell and restart this daemon
    ok    after a reboot: lingering is on: the daemon starts without anyone logging in
    ok    sleep: not once since it booted (6 days); a sleeping computer is unreachable from the phone
    ok    disk: 412 GB free of 931 GB where transcripts and state live
    ok    claude: 2.1.270 (Claude Code)
    ok    sessions: 3 running now, 3 of them in tmux (registry ~/.claude/sessions)
    ok    transcripts: 12 project directories under ~/.claude/projects
    ok    changes: git version 2.51.0 (what changed, on the phone)
    ok    capture: grim, wf-recorder
    ok    daemon: portholed 0.28.0 answering, 1 paired device(s), 1 connected now
    ok    service: portholed.service active, starts at login
    warn  approvals: hook not registered: approvals stay at the desk (portholed install-hooks)
    info  builds: 1 published in ~/.config/porthole/builds

    The daemon's log: journalctl --user -u portholed -f

    fail is something the phone cannot work without, warn is a feature the phone will not have, and info is context.

  6. Which computer?

    On the phone. Type the computer's name on your tailnet, such as workstation, or its Tailscale IP address, and tap Check connection. Porthole checks that portholed is actually answering before it asks you for a code: Found Porthole on workstation.

  7. Authorize

    On the phone. Authorize Porthole lists exactly what this phone will be able to do on that computer: run commands, read and write files, approve tool calls, and read session history; and, when the computer offers them, capture your screen, share a dev server, and start Claude Code. Read it, then tap Authorize.

  8. Pair

    On the computer. Run portholed pair. It prints a QR code and a 6-digit code. The code works once and expires after five minutes; run the command again for a new one. -no-qr prints the code alone, and -png FILE also saves the QR code as an image.

    portholed pair

    On the phone. Tap Scan the QR, or type the six digits, then Pair. Scanning fills in both the address and the code, so nothing is typed. The phone's own camera can open the QR code too: the app fills in the form and waits for you to press Pair. It never connects on a link's say-so.

    Scanning uses Google's code scanner from Play services; typing the code avoids it. See the privacy policy.

  9. The tour, then your sessions

    On the phone. Paired. A four-page tour shows the feed, approvals, the terminal and the failsafe, with a working example on each page; Skip goes straight to your sessions. The tour stays in Settings under Help, as How Porthole works.

What installing does

On a Mac

brew install shrimpscript/tap/porthole installs the release's portholed for your Mac (Apple silicon or Intel), which Homebrew checks against the release's checksum, links porthole beside it, and installs tmux. Then portholed setup:

  1. Writes and loads a launchd agent, ~/Library/LaunchAgents/dev.shrimpscript.portholed.plist, which starts portholed serve at login and restarts it if it fails. It carries your shell's PATH and UTF-8 locale, since launchd gives an agent neither; its log is ~/Library/Logs/portholed.log.
  2. Creates ~/.config/porthole for the device list and published builds, and ~/.ssh if it is missing, for the failsafe key.
  3. Registers the PermissionRequest hook in ~/.claude/settings.json (see Approvals). portholed setup -no-hooks skips it.

To remove it all: portholed uninstall-hooks, portholed service uninstall, then brew uninstall porthole. Paired devices stay in ~/.config/porthole until you delete it.

On Linux

  1. Checks for tailscale and tmux and stops if either is missing, looks for claude, and checks that Tailscale is connected.
  2. Builds portholed with Go if Go is installed. Otherwise it downloads the binary from the latest release and refuses it unless it matches the release's SHA256SUMS. It installs to ~/.local/bin, with porthole linked beside it.
  3. Creates ~/.config/porthole for the device list and published builds, then installs and starts a systemd user service, portholed.service, enabled at login. It runs with ProtectSystem=strict: the only paths it can write are that directory and ~/.ssh, for a failsafe key.
  4. Checks whether lingering is on, and tells you the loginctl line if it is not.
  5. Registers the PermissionRequest hook in ~/.claude/settings.json (see Approvals). Undo it at any time with portholed uninstall-hooks.

To remove it: portholed uninstall-hooks, portholed service uninstall, then delete ~/.local/bin/portholed and the porthole link.

The installer's options (it runs on a Mac too, for anyone without Homebrew):

OptionWhat it does
--dry-runPrints every action and changes nothing.
--prebuiltUses the release binary (macOS or Linux, amd64 or arm64), checked against SHA256SUMS, even when Go is installed.
--no-hooksSkips the Claude Code hook. Remote approvals stay off until you run portholed install-hooks.

Revoke a phone

portholed devices          # who is paired, with an ID per device
portholed revoke <ID>      # remove it and drop any live connection, at once

Revoking works whether or not the phone cooperates. The phone shows This device was revoked and offers to pair again. Unpair this phone in Settings forgets the computer on the phone's side only; to cut the phone off from the computer's side as well, run portholed revoke there.

More than one computer

Settings › Another computer › Add a computer pairs the phone with a second computer that runs portholed. Its sessions join the list, named after the computer they run on, and a computer that cannot be reached is kept apart from the rest. Each computer has its own pairing and is forgotten on its own.

Sessions

The list shows every running Claude Code on the computer, one row per process, plus the most recent transcript in directories where nothing runs. It is sorted by attention: Needs you (a question waiting on you), then Live (working sessions first, with what each one is doing and for how long), then Recent.

Each live row names its tmux session, so two sessions in the same directory stay apart. Sessions are identified by the process that writes each transcript (Claude Code keeps a registry of them), so a second Claude started in the same folder is a second row with its own feed, never a merge. A session's feed follows only its own restart in the same tmux pane.

The ring on each row is honest state: a closed ring is live, a turning ring is working, an amber ring is waiting on you, a dim ring is idle. The ring in the header is the connection to the computer: connected, reconnecting, or dropped. Nothing pulses to look alive. A dot beside a title marks a session that did something since you last opened it.

The timer on a working row counts the whole turn, not the current tool call. A local command such as /model, /effort or a ! shell command is not a turn, and does not make a session read as working.

Widget and quick tile

A home-screen widget lists the sessions and what each one is doing; a quick-settings tile shows the count. Both say how old their view is when the app is not connected, and neither claims to be connected once the app's process has been gone for more than a couple of minutes. Settings › Home screen adds the widget; the tile is in the quick-settings edit list, under Porthole.

The feed

Open a session for its feed. Every row comes from a record in Claude Code's own transcript; a record that cannot be mapped is left out, never invented. Your prompts sit on the right. Claude's words are rendered markdown: what it says on the way, between pieces of work, is quieter and set off by a line, and its answer at the end of a turn is full strength, with Copy, Listen (the phone reads it aloud with its own voice, offline, skipping code) and Share beneath it. New replies fade in from the top down.

The work Claude does between its words folds into one line per stretch, as in Claude's own app: Ran 2 commands, edited a file, with the lines its edits added and removed (Claude Code's own count), and how many steps failed. Tap it for every step, in order, with each one's time or lines and, for a step that failed, its error; tap a step for its command and full output. While a step runs, it is a line of its own beneath, with the turning screw and its time, and it folds in when it finishes. A turn that edited files ends with a Diff chip with that turn's lines, then the CLI's own turn line; the chip opens Changes, which shows everything uncommitted in the folder, not only that turn.

A finished turn, with quick replies above the composer.
While Claude works: the working strip, and Interrupt.
  • Working strip. While a turn runs: the CLI's own working line, elapsed time, tokens, and an Interrupt button (Esc).
  • Feed and terminal. The terminal button in the session's header switches to the terminal; the button there switches back. The chip beside it says how full the context window is and opens Session details.
  • Earlier. The feed opens with the last sixty rows; the pill at the top loads more without losing your place.
  • New below. If you scrolled up while rows arrived, a pill counts them; tap it to jump.
  • Since you left. A session that ran on without you opens on a line across the feed; see Since you left.
  • Sending. The message box is one box: the text on top, and beneath it + to attach, the model and effort (tap for the switches) and send. Type / for Claude Code's commands with their descriptions, or attach a photo or a file with +: a PDF, a log, a spreadsheet, anything up to 10 MB in all per message. The keyboard's send key submits. Prompts are typed into the session's tmux pane exactly as written. A message stays in the box, greyed, until the computer says it typed it, which is a moment on a good connection; if it was refused, or the connection went first and the feed shows it never arrived, it stays there to send again, and nothing is ever re-sent on its own. Attachments are saved in ~/.config/porthole/uploads on the computer, never in the project, named in the prompt so Claude can open them, and removed after two weeks. A message whose files could not be sent comes back to the box.
  • Files. Type @ and the session's files are listed above the composer, the most recently changed first; keep typing to narrow them by name or by part of the path, and tap one to put its path in the message. Claude Code reads a file named this way along with the prompt. In a git repository the list is git's (tracked files, and new ones that are not ignored); elsewhere it is the folder's files a few levels down, without hidden ones.
  • Quick replies. When Claude is idle and nothing is being asked, a row of one-tap sends sits above the composer: Continue, Yes, No, Looks good. The pencil edits the four.
  • Agents. When Claude hands work to a subagent, the feed shows a card for it: what it was asked to do, what kind of agent it is and on which model, and, read from the agent's own transcript on the computer, how many tools it has used, what it is doing now and for how long. A finished, failed or stopped agent says so. The working strip counts the agents at work; background agents that keep going after Claude's turn has ended get a strip of their own. Tap either for every agent in the session, each agent's own agents under it.
  • Pictures and files you sent. A message that carried files shows them beside its bubble: pictures as thumbnails (tap to open), other files as chips with their names. An upload the computer has cleared, after two weeks, says so on its tile.
  • When the connection drops. Nothing on screen goes: the feed, where you had scrolled to, an open sheet and the message you are typing all stay, and the box keeps taking typing. After a second or two a bar says Reconnecting to workstation…; after a few tries it offers Options, the failure card's tools (retry, the SSH failsafe, restarting the daemon). When the computer answers, what happened meanwhile is added below what you were reading, with no reload and no jump. The list stays too, each row reading "as last seen". The full failure card takes over only when there is nothing to show, or when the computer refuses the phone (unpaired, revoked).
  • Saved on the phone. Each session's draft is kept as you type, so it survives a drop, the app closing and the phone restarting. The last 200 rows of the 20 sessions you opened most recently, and the session list, are kept too: a session opens at once from its copy (the bar says when it was saved, while the computer cannot be reached) and catches up when the computer answers. See what the app stores.
  • Commands. A slash command run in the session, at the desk or from the phone, is a card showing what it did rather than the CLI's echo: /effort with the level on a five-step meter and whether it is now the default for new sessions, /model with the model by name, /rename with the new name, and /context as the window to scale with what fills it, largest first. Any other command shows the CLI's reply, in red when it came as an error, and a skill run as a command says so. A compaction and a /clear are lines across the feed, with the tokens a compaction freed. Typed on their own, /model, /effort, /cost, /stats and /status open Session details instead of a menu on the computer's screen.

Since you left

In the list, a dot beside a title marks a session with activity you have not seen. Open it, and a session that ran on while you were away lands on a line across the feed: 3 turns since you left · 4h ago. The line sits above the first row that arrived after your last visit, so everything below it happened while you were gone, in order.

With three or more new rows, the feed opens on the line rather than at the bottom. A turn that is still running is not counted yet; until it ends, the line says Since you left.

When you last opened each session is kept on the phone, and only there.

The model chip in the header opens Session details, which is also where a session is muted.

Questions, answered by tap

When Claude Code asks you to choose (its AskUserQuestion tool), the picker on the computer's screen appears on the phone as a card above the composer, with every option, its description, and the picker's own “Type something”. The card is read off the live tmux screen, so it shows what the picker shows. Tap an option and the daemon presses that number in the picker.

Multi-select questions show ticks that move as you toggle, then Next. Several questions in one call walk through in order and end on the CLI's review screen, where “Submit answers” is one more tap. A tap after you answered at the desk is refused, not typed into the prompt.

If the picker cannot be read from the screen, the phone still shows the question, with Answer in the terminal, which opens the Terminal view where the picker is. The daemon also keeps a session's window large enough to read while nobody is attached to it at the desk.

The session sorts under Needs you the moment a picker is up, and a notification says what is being asked. The feed keeps the question and your answer as history: “Claude asked …”, then “Answered: Always light”.

Approvals

When Claude Code asks permission to run a tool, the request reaches the phone as a card over the session: what Claude wants to do, the command in full exactly as it will run, the directory, and how long is left. Allow or Deny. Nothing is ever approved on your behalf, and there is no “always allow”: a request the phone does not answer within 90 seconds falls back to the prompt at the desk.

This needs the hook. portholed install-hooks adds a PermissionRequest hook to your Claude Code settings (~/.claude/settings.json), keeping whatever is already there; portholed uninstall-hooks removes exactly that entry. The installer registers it unless you pass --no-hooks.

The hook fails open: if the daemon is down, Claude Code behaves as if the hook were not there. If you run Claude Code with permissions bypassed, there is nothing to approve and the hook is idle.

The terminal

Not a chat box pretending. The Terminal tab is a VT100/xterm emulator attached to the session's tmux window, showing the same screen you would see at the desk, at the desk's own width: vim and btop look like vim and btop. The phone's view is a mirror in the same tmux session group, so opening it never resizes the window on the computer.

The key row is built for Claude Code rather than for a generic server: Esc, Tab, ⇧Tab (permission modes), Ctrl, Alt, ^C, the /, @ and ! menus, the arrows, ^B (the tmux prefix), and a panel with Home, End, PgUp, PgDn, Del, F1 to F12, the symbols a phone keyboard hides, and the common Ctrl combinations. Modifiers are sticky: tap to arm for the next key, tap again to lock, tap again to clear. Armed and locked look different, so a locked Ctrl never surprises you.

Fit shrinks the whole window to the phone; minus and plus set the text size, and bigger text scrolls sideways rather than rewrapping. The default size is in Settings › Terminal. Tap the screen to raise the keyboard. Turn the phone sideways and a live session opens in the terminal, since landscape means you turned it to type.

Scrolling back. Drag up or down with one finger to walk the window's history. It is tmux's own scrollback (copy mode), so the pane at the desk scrolls with it, as it would with a mouse wheel; a chip shows how far back you are and returns to the live screen, and typing returns there first. Full screen (the corner button) hides the header and the system bars; a swipe from the edge shows them, and Back leaves full screen. When the window is resized at the desk, the phone's grid follows within a couple of seconds.

Changes

The Changes icon in a session's header opens what git sees in the session's directory: every changed file with its status (new, modified, deleted, renamed) and its added and removed line counts. Tap a file for its diff against the last commit, line by line, additions and removals tinted, with sideways scrolling for long lines. Untracked files show as additions; renames are paired with their old path.

The list is fetched when you open it and when you refresh it, never on a timer.

The daemon runs only read-only git commands (status, diff, rev-parse), only in a listed session's directory, and bounds the output (80 files, 96 KB per file), so a stray 64 MB log costs nothing. A failure is reported as a failure, not as a clean tree.

Claude account

Claude Code keeps one sign-in per user on a computer, so the account is the computer's, for every session on it. Settings › Claude Code shows which account that is and its plan, with Switch account and Sign out; Sign out asks first, in place, and sessions cannot reach Claude until the computer is signed in again. The usage-limit card offers Switch Claude account too.

Switch account starts Claude Code's own sign-in (claude auth login) on the computer and opens Claude's sign-in page in the phone's browser. Sign in there with the account you want, copy the code the page shows, and come back: Porthole recognises that page's code on the clipboard (it ends with the link's own state, so no other text is taken for it) and finishes the sign-in, or you can paste it. The page signs in with whichever Claude account the browser uses, so for another one sign out of claude.ai in the browser first, or open the link in a private tab (Copy link). Nothing opens on the computer, and Porthole keeps no copy of the code.

Sessions already running may keep the account they started with. Once signed in, Porthole lists them and offers to restart them: each one's Claude Code stops and comes back in the same tmux pane on the same conversation (claude --resume), signed in as the new account. A session in the middle of a turn, a ! command, a question or a permission prompt restarts when it is free, and Cancel calls that off; one waiting out a usage limit stops waiting, since another account is why it is restarting. A session whose Claude Code was not started from its tmux pane's own shell is left running, for you to restart yourself.

Session details and switches

The chip in a session's header shows the model and how full the context is, such as Opus 5 · 6%. Tap it for Session details; when the header has no chip yet, the i opens the same sheet. From the top:

  • the session's name, with a pencil to rename it while Claude Code is running there: it types Claude Code's own /rename, so the name is the same at the desk;
  • the model and the permission mode;
  • the context window: what the last request carried, out of the model's window; under it, what fills it by category, from the feed's latest /context, with a button to measure again (the answer lands in the feed and here);
  • tokens this session: input, output, thinking, cache read and cache written;
  • when Claude Code has written its own accounting: the API-equivalent cost and time, and lines added and removed;
  • activity: turns, your prompts, replies, tool calls, and the tools used most;
  • when the session started and last did something;
  • the switches, this session's notifications, and one-tap /cost, /status and /usage, which open in the Terminal view where the CLI prints its answer.
The top of the sheet.
Switches, and this session's notifications.

The switches type the CLI's own commands or press its keys:

SwitchWhat a tap doesWhere the current value comes from
Model/model fable, opus, sonnet or haikuthe latest reply in the transcript
Effort/effort low, medium, high, xhigh or maxthe CLI's own effort line, shown after a turn; before that, the saved default, and the row says so
PermissionsShift-Tab, which cycles bypass → auto → manual → accept edits → planthe CLI's bottom line

The highlight follows the CLI's confirmation, not the tap. Model and effort changes also become your defaults for new sessions, because the CLI saves them, except max effort, which the CLI keeps to this session; the sheet says so under the switches.

Start and resume from the phone

A new session, anywhere. The + at the top of the session list opens New session: every folder Claude Code has been used in on the computer, most recent first, and a field for another folder (a full path, or one starting with ~/). Pick one and the computer starts Claude Code in a tmux session of its own, named after the folder - app, then app-2 for a second task there - exactly as porthole does at the desk. The phone opens it as soon as Claude Code is up, and tmux attach -t app reaches the same session at the desk. It works the same on a Mac and on Linux.

A session whose Claude Code has stopped shows a card with two choices. Resume continues that transcript; New session starts fresh in the same directory. The daemon uses the tmux window that already sits in that directory when it holds an idle shell, and opens a new window there otherwise; it never types into a window running something else.

A folder Claude Code has never been used in asks, at the start, whether you trust it; answer in the Terminal tab. Naming a folder gives the phone nothing it did not have: a paired phone can already run commands on the computer.

Notifications

  • Working. While a turn runs in a session you are not looking at: an ongoing notification with a running timer and what the session is doing, promoted to the status bar where Android allows it.
  • Done. When the turn ends: “Done · worked for 4m 12s”, with Reply, which sends the session's next prompt from the shade without opening the app.
  • Asking you. The moment Claude asks a question: a high-priority notification with the question. It clears when the question is answered.
  • Usage limit. When the CLI reports a limit, with what it says it will do next. A usage limit that stops a task is always announced.
  • Your computer is back. After the connection was down for two minutes or more, when the computer answers again and the app is not on screen.

None of them are posted for the session on screen. Settings › Notifications turns the working and done notifications off; questions ring whenever Android allows Porthole's notifications at all. To silence one session instead, mute it.

Settings › While you are away says whether Android's battery optimisation can cut Porthole's connection while the phone is idle, which is when notifications stop arriving, and opens the battery settings to exempt it.

Mute a session

A loop that finishes a turn every four minutes trains you to ignore the status bar. Open that session's details and turn off Tell me about this session. It goes silent: no working timer, no finished turn, no question waiting. It stays in the list and in Needs you, because hiding it would be a different thing. Anything it had already posted is withdrawn.

Muting is per session and kept on the phone. Settings › Notifications says how many sessions are muted, and Unmute all turns them all back on in one tap.

Preview a dev server

The globe in a session's header lists what is listening on the computer's loopback, such as a Vite or Next dev server or a docs server. Pick one and the daemon shares it through a proxy bound to the tailnet address, gated like everything else, and the phone's browser opens it. Links to localhost:PORT in Claude's replies open the same way. Stop the share from the same sheet; a share whose server has exited closes itself.

Screen capture

The camera in a session's header takes a screenshot of the computer's screen, which lands in the feed as a row, or records a 5 or 10 second clip. Only when you ask, and the Authorize screen says so. Screens that have gone to sleep are woken for the capture and put back to sleep after it, and the picture's caption says so; on a Mac the display wakes and goes back to sleep on its own timer. A live view of the desktop is planned as a remote desktop of its own.

On a Mac it uses the system's own screencapture, of the main display. macOS asks once, at the Mac, to let portholed record the screen (System Settings › Privacy & Security › Screen & System Audio Recording); until it is allowed, a capture fails or shows only the wallpaper. Clips are shrunk with ffmpeg when it is installed, otherwise with the system's avconvert.

On Linux it needs a Wayland desktop with grim for screenshots and wf-recorder for clips; with more than one monitor, Hyprland's focused one. Waking sleeping screens works on Hyprland and sway.

Themes

Three, in Settings › Appearance: Porthole's own dark palette, the default and the one on this page; a Claude look, light and dark, with a serif for Claude's replies; and a Gemini look, light and dark. The app follows the system's light or dark setting unless you pin one. In the terminal, set Claude Code's own /theme to match.

Builds from your projects

If Claude Code is building an Android app on your computer, you can put each build on your phone without a cable or a file transfer. Publish it on the computer and Porthole offers it with an install banner.

Publish a build

portholed publish app/build/outputs/apk/release/app-release.apk 1.4.0 shopping-list

Three arguments: the APK, its version (dotted numbers, like 1.4.0), and the app's name as a lowercase slug. The slug becomes the name on the phone: shopping-list shows as Shopping list.

published Shopping list 1.4.0 as shopping-list-1.4.0.apk (8421376 bytes) to ~/.config/porthole/builds
paired phones will offer it on their next connection
or in the phone's browser: http://192.0.2.10:8737/builds/

It is an ordinary command, so Claude Code can run it as the last step of a build. To make that a habit, say so in the project's CLAUDE.md, for example: “After a release build, run portholed publish with the APK, the versionName and shopping-list.” When aapt2 is on the PATH or in an Android SDK, publish also reads the APK's own version and warns if it differs from the one you typed.

The install banner

The phone learns about builds when it connects to the computer. On its next connection it shows a banner above the session list, Shopping list 1.4.0 is on the computer, with Not now and Install. The line under the title says what happens next: it downloads from your computer, then Android asks to install or update it.

Install downloads the APK from your computer over your tailnet, behind the same pairing gate as everything else, with its progress on the banner, then hands it to Android's installer. The newest build of each app is offered once: after you install it, or tap Not now, the banner stays away until a newer version is published.

The banner, above your sessions.
Android asks before every install.
Settings keeps every published app one tap away.

Android's one-time permission

The first time you tap Install, Android has not yet allowed Porthole to install apps. Porthole opens that setting (Install unknown apps) and the banner says “Allow Porthole to install apps, then tap again.” Allow it, come back, and tap Install again. That permission is asked once. Android's installer still asks before every install or update, so nothing installs silently.

Android installs an update only when the new APK is signed with the same key as the version on the phone. A debug build cannot update a release build of the same app; uninstall one first.

Other apps on the computer

Every app with a published build is listed under Settings › Other apps on the computer, with its version, its size and Install, so a build you put away is still one tap from the phone.

A browser on a paired phone can also open http://<computer>:8737/builds/, a plain page listing every published build with Download. It is gated like everything else; any other device gets “this device is not paired with portholed”.

Porthole itself is not offered this way. Its own new versions come from GitHub releases; see Updates. A paired computer can offer the phone any APK, and Android asks you each time, so pair only computers you control.

Updates

Porthole updates from its GitHub releases.

The check. At most once a day, when the app opens or comes back to the screen, and whenever you tap Check now, the app asks api.github.com for the latest release of ShrimpScript/porthole. It is one unauthenticated request. It carries nothing about you, the phone or your computer beyond what any web request carries: your IP address reaches GitHub, as it does when you open any web page. Drafts and pre-releases are never offered.

The banner. When the release is newer than the app, the list shows Porthole 0.27.0 is out with Not now and Update, and under the title the version you have and where the download comes from.

The download. Update downloads porthole.apk from the release over HTTPS, from GitHub only: a redirect to any other host, or down to plain HTTP, is refused. Then Android's installer asks you. Android installs the update only if it is signed with the same key as the app you have, so a substituted APK is refused whatever its source.

The switch. Settings › Updates › Check GitHub for new versions turns the check off. The same section shows your version and what the last check found.

Store builds. A build of the app made for a store has the check compiled out and updates through the store. There is no store listing yet.

Updating the daemon

The daemon does not update itself.

On a Mac

Upgrade it with Homebrew, then restart the service so the new binary is the one running:

brew upgrade porthole
portholed service restart

Each new version is a new program to macOS, so it may ask again about your folders and the screen. Run portholed setup at the Mac after an upgrade to get those questions over with; it also rewrites the service when a release changes it.

On Linux

In the clone, pull and run the installer again; it rebuilds with Go when Go is installed (add --prebuilt for the release binary) and rewrites the service. Then restart it so the new binary is the one running:

git pull
./tools/install.sh
portholed service restart

Installed with Homebrew instead: brew upgrade porthole, then portholed service restart.

If the computer runs a newer portholed than the app understands, the app says workstation runs a newer Porthole; update the app from Settings › Updates.

The failsafe

If the daemon stops answering, the app says so: Can't reach workstation, with Retry, Open terminal anyway and Restart the daemon over SSH. While it keeps retrying on its own the card says that too, and it clears itself when the computer answers. The two SSH buttons go over SSH, which does not depend on Porthole running. Restart runs the command the daemon reported while it was up - systemctl on Linux, launchctl on a Mac, and the daemon on its own if the service manager refuses - and reconnects. Recovery does not require walking back to the desk.

The same shell is in Settings › If Porthole cannot connect › Open a shell over SSH, so it can be tried while the computer is still in the room. Before a trip that is worth one tap: a login that works at home works from anywhere on the tailnet, and the moment the daemon is down is the wrong time to find out that SSH was never enabled.

With Tailscale SSH (Linux: tailscale up --ssh, or tailscale set --ssh), the tailnet signs the phone in; it needs a rule in your tailnet's access controls that allows it. If the rule is set to “check”, Tailscale asks for a browser sign-in every 12 hours and the app tells you; setting the rule to “accept” removes that.

With the phone's own key, where there is no Tailscale SSH: a Mac, whose Tailscale app has no SSH server, or a Linux computer with its own sshd. Turn on Remote Login (System Settings › General › Sharing), then in the app tap Add this phone's key under Settings › If Porthole cannot connect, while Porthole is connected. The phone makes an Ed25519 key, keeps it encrypted under a key in Android's Keystore, and the daemon adds the public half to ~/.ssh/authorized_keys as one line, tied to the phone's tailnet addresses, allowed a terminal and no forwarding. Remove this phone's key takes it out, and so does portholed revoke. Nothing else in the file is touched.

portholed doctor reports either on its ssh failsafe line, with how many phone keys there are.

The shell is your login shell, sized to the phone and following rotation and the keyboard, with its own scrollback (drag to scroll) and a full-screen mode. It runs with PORTHOLE_FAILSAFE=1 set: if your shell profile attaches to tmux (or anything else) on every SSH login, skip that when the variable is set, so the failsafe stays a plain shell.

After a drop of two minutes or more, the app tells you when the computer answers again, so a phone in a pocket does not have to be checked.

The shell from Settings, before anything breaks.

Troubleshooting

What you seeWhat it meansWhat to do
“Can't reach workstation”The daemon is not answering on the tailnet address. Tailscale may be off, the computer asleep, or portholed stopped.portholed doctor at the desk, or Restart the daemon over SSH from the card (see The failsafe).
“Not recognised on the tailnet”The daemon could not identify this phone through Tailscale.Check that Tailscale is connected on the phone and that both devices are on the same tailnet.
“That code didn't work”The pairing code was wrong or has expired.Run portholed pair again for a new one.
“This device was revoked”The computer removed this phone.portholed pair again.
“the computer did not accept this phone's key”The key was removed there, or the phone was set up again.Settings › If Porthole cannot connect › Add this phone's key, while Porthole is connected.
Remote Login is off (a Mac)Nothing answers for SSH, so the failsafe has no way in.System Settings › General › Sharing › Remote Login.
Claude Code started from the phone says "Operation not permitted" (a Mac)macOS has not let portholed use the folder: Desktop, Documents and Downloads are guarded, and a Claude Code the daemon started works in them as portholed.At the Mac: portholed setup, and allow the questions; or System Settings › Privacy & Security › Files and Folders › portholed. portholed doctor shows it on its folders line.
A screenshot shows only the wallpaper (a Mac)macOS has not allowed portholed to record the screen.At the Mac: System Settings › Privacy & Security › Screen & System Audio Recording, allow portholed.
“Tailscale wants you to sign in again”The tailnet's SSH rule is set to “check”.Sign in, or set the rule to “accept” in the Tailscale admin console.
“workstation runs a newer Porthole”The daemon is newer than the app.Settings › Updates › Check now.
“Claude Code isn't running here”The session's transcript exists, but no Claude process is writing it.Resume or New session on the card, or start it at the desk inside tmux.
“This session is not in tmux”Claude Code runs outside tmux: it can be watched, not typed to.Start it with porthole instead of claude next time; portholed doctor lists sessions outside tmux.
Two sessions in one folder look mergedAn older Claude Code without a session registry.Update Claude Code; the registry (~/.claude/sessions) is what tells them apart.
No question card, though the terminal shows a pickerThe screen read failed, or the pane is not the registered one.Check that portholed sessions shows the session as live.
“Claude is not asking anything right now”You tapped after the picker was answered at the desk.Nothing; the tap was refused rather than typed.
Approvals never reach the phoneThe hook is not registered, or Claude Code runs with permissions bypassed.portholed install-hooks; check portholed status.
The phone goes quiet while it is idleAndroid's battery optimisation cut the connection.Settings › While you are away › Open battery settings.
Nothing after you log out, or after a rebootLinux: lingering is off, so the user service stops with your session. A Mac: the agent runs in a login session, and nobody has logged in since the reboot.Linux: sudo loginctl enable-linger $USER. A Mac: log in, or turn on automatic login in System Settings › Users & Groups.
No banner after portholed publishThe phone learns about builds when it connects, so a build published while it was connected arrives with its next connection. Each version is offered once.Open http://<computer>:8737/builds/ in the phone's browser to get it now, or install it later from Settings › Other apps on the computer.
Android will not install a buildIt is signed with a different key than the version on the phone.Uninstall the app on the phone first, then install the build.

portholed commands

CommandWhat it does
porthole [ARGS]Starts Claude Code inside tmux, where the phone can reach it: a session named after the folder, or the one already open there. ARGS go to claude. The same program as portholed (the installer and Homebrew link the name); portholed claude does the same.
portholed setupInstalls and starts the background service and registers the approval hook, once; what the installer runs, and the step after brew install. -no-hooks leaves Claude Code's settings alone.
portholed service install|restart|status|uninstallThe background service on its own: a systemd user service on Linux, a launchd agent on a Mac.
portholed serveRuns the daemon, bound to the tailnet addresses only. The user service does this. -port sets the port (8737); -v logs more.
portholed pairPrints a single-use 6-digit code and its QR code for a new phone. -no-qr prints the code alone; -png FILE also writes the QR code to a file.
portholed devicesLists paired devices: name, tailnet user, when paired, last seen, live connections, and the ID.
portholed revoke IDRemoves a device and drops its live connections at once.
portholed statusThe daemon's version, paired and connected devices, and whether the approval hook is registered.
portholed doctorChecks everything a phone needs on this computer, one line each: tmux, Tailscale, key expiry, the SSH failsafe, what happens after a reboot, sleep, disk, Claude Code, sessions, git, capture, the daemon, the service, the hook and builds. Exits with an error if anything failed.
portholed sessionsLists Claude Code sessions as the phone will see them. It reads the files directly, so it works while the daemon is down.
portholed publish APK VERSION APPOffers a build of an Android app you are working on to paired phones (see Builds).
portholed install-hooksRegisters the PermissionRequest hook in ~/.claude/settings.json, keeping everything else in it.
portholed uninstall-hooksRemoves exactly that hook again.
portholed replay FILEMaps a transcript to feed rows, for checking what the phone would show. -feed prints the rows, -json the counts, -limit N only the last N.
portholed versionPrints the version.
portholed hookRun by Claude Code when it asks permission; not for you to run.

Security and privacy

  • No LAN, ever. The daemon binds the computer's tailnet addresses only. The LAN address and localhost refuse the connection.
  • Two gates. Every connection is identified through tailscaled's WhoIs (which node, which user), and the node must also be on the paired-device list, which only a code shown on the computer can add to. Codes are single-use, expire after five minutes, and allow a limited number of attempts.
  • No browsers. Any WebSocket request carrying an Origin header is refused, so a web page open on a paired phone cannot drive the daemon.
  • Read-only where it can be. Discovery reads transcripts and Claude Code's own registry; Changes runs read-only git; captures run only when you ask.
  • Hooks fail open. A hung daemon never stalls Claude Code at the desk, and a timed-out approval is never an allow.
  • Installs go through Android. Porthole's updates come from GitHub releases over HTTPS and must be signed with the same key; builds of your projects come from your paired computer. Android asks before every install.
  • No telemetry. No account, no backend, no analytics. The only place the app reaches besides your computer is GitHub, for its own updates, and that check can be turned off.

The full statements are the privacy policy and SECURITY.md, which also says how to report a vulnerability privately.