Back to Blog

Claude Code on an iPad: What Actually Works

Every stack the guides recommend — Termius, mosh, tmux mouse mode, Remote Control — fails on iPad for structural reasons. Here's the tested setup that finally works, and the tmux version gotcha nobody documents.

Claude Code on an iPad: What Actually Works
Matt Owens
Matt Owens
8 Jul 2026 - 10 min read

July 2026. Setup: Linux desktop at home, Tailscale, iPad with a Magic Keyboard, wanting to run a coding agent from the couch.

I spent two days getting Claude Code to work properly from an iPad. Not “mostly works if you don’t scroll” — actually work: read long agent output, select text, survive disconnects. I tested every option the guides recommend, and every one of them fails — not because of bugs, but because of architecture. That distinction matters, because you can wait out a bug. You can’t wait out an architecture.

Here’s each option, why it fails, and the stack that works.

The standard advice, and where each layer breaks

Every guide teaches the same stack: SSH client (Termius or Blink) + Tailscale + tmux + mosh. I was running exactly that. The first symptom was jumbled text when scrolling up. Each layer under it failed in its own way.

tmux: repaints in place, so your terminal’s scrollback is garbage

tmux doesn’t let output flow off the top of the screen — it repaints the screen using cursor-positioning escape codes. Your terminal app’s native scrollback therefore fills with overlapping repaint fragments: status bar copies, half-drawn lines, stale screens. That’s the jumble.

The prescribed fix is set -g mouse on, which routes scrolling through tmux’s own buffer. It works — and it breaks selection, because now tmux owns every touch. Drag-select stops auto-scrolling at the screen edge: a desktop mouse keeps emitting movement events while pressed against the edge of the screen; a finger hits the bezel and goes silent. There is no modifier key on glass to escape to native selection. Desktop users never notice any of this, which is why the guides written on desktops don’t mention it.

One trap I hit along the way: raising history-limit does nothing for existing panes, and respawn-pane doesn’t apply it either — the buffer size is fixed at pane creation. I tested this on a throwaway server after respawning didn’t help. Only new panes get the new limit.

mosh: architecturally incapable of scrollback

mosh doesn’t stream bytes; it synchronizes screen states. When output scrolls past between sync frames, those lines are never transmitted at all — there is nothing on your device to scroll back to. This is mosh issue #122: open since 2012, 272 thumbs-up, and the maintainers have said plainly that it’s structural. Fourteen years is long enough to believe them.

It also means fast output (like dumping a long transcript) skips frames on the client. No terminal app can fix this — Termius, Blink, and Moshi all inherit it the moment you choose mosh as transport.

The iOS terminal apps: wrappers around the same two broken options

I tested Termius and Moshi directly and read Secure ShellFish’s and Blink’s docs and trackers. Every one of them ultimately offers the same choice: your terminal’s local buffer (jumbled under tmux, empty under mosh) or tmux mouse mode (selection breaks on touch). The apps differ in polish, not in architecture.

Anthropic’s Remote Control: a chat mirror, not a terminal

Claude Code’s first-party answer (/rc in a session, then steer from the Claude iOS app or claude.ai/code) mirrors the conversation — messages, tool calls as cards, approve/deny buttons. Scrolling and selection are native because it’s a real app UI. But there’s no shell, no TUI, no way to see what’s actually happening on the box beyond what the transcript shows. It’s fine for approving a permission prompt from a supermarket queue. It is not a place to live.

The missing primitive

There’s been a correct answer to this problem for over a decade — on exactly one platform. iTerm2’s tmux integration (tmux -CC, “control mode”) has the terminal app speak tmux’s text protocol and rebuild windows and panes as native UI. Scrollback becomes real app scrollback. Selection becomes real selection. tmux keeps doing what it’s genuinely great at: keeping sessions alive on the server.

People want this everywhere: the open feature requests total roughly 840 reactions across Ghostty (477 and counting — they’re actively building it), Windows Terminal, WezTerm, and Alacritty. On iOS, nothing implemented it.

Until a few weeks ago.

rootshell: tmux -CC on the iPad, and my test results

rootshell is a free iPad/iOS terminal built on libghostty (the engine behind the Ghostty desktop terminal) that ships real tmux control mode: windows become native tabs, panes become native splits, and scrollback and selection are handled by the app, not forwarded escape sequences.

It’s new and barely covered anywhere, so here’s what I measured with Claude Code specifically, over SSH + Tailscale to a Linux box running tmux 3.4 — including the part where I fooled myself, because you will hit the same trap.

The trap: you can think you’re in control mode when you aren’t. A plain tmux attach (or, currently, the app’s connect popup) gives you a regular attach — ordinary terminal-emulated tmux with all the touch problems this post is about. I spent hours “testing rootshell” in that mode and blaming the app for tmux mouse-mode behaviors: selection that only advanced in a narrow band at the screen edge, the view rubber-banding to the bottom on every one of Claude’s repaints. The tell is simple: in control mode there is no tmux status bar and windows appear as native tabs. If you can see the colored bar at the bottom, you’re in the legacy world. (Server-side check: tmux list-clients -F '#{client_control_mode}' — you want 1.)

In genuine control mode, measured properly:

  1. Scrolling holds. The transcript lives in the app’s buffer; the view stays put even while Claude repaints under it. No jumble, no copy-mode, no rubber-band.
  2. Selection just works — native, with edge auto-scroll.
  3. Claude Code’s TUI renders clean. This was my biggest open question — Claude Code’s renderer is notorious for fighting tmux (rendering corruption, a scrolling-issue cluster with thousands of reactions). Under -CC, streaming output rendered without corruption.
  4. Persistence works. Kill the app entirely, reopen, reattach — the session (and the running agent) is exactly where you left it. tmux does the work; no mosh needed.

The one real bug I hit — and how it resolved. In my first sessions, tmux -CC attach sometimes produced a blank screen when the active window was running Claude Code, even though the server showed a healthy control-mode client. Capturing the raw control-mode stream server-side (script -c 'tmux -CC attach -t main' log) revealed the prime suspect: Claude Code wraps every frame in synchronized output (ESC[?2026h … ESC[?2026l), about ten times a second even when idle — sequences the developer’s static test panes never emit. Reported as rootshell#246, where the developer responded within the hour, confirmed the mechanism (“a pane stuck with mode 2026 ON stays permanently blank”), and dropped a key hint: older tmux handles sync-output differently than newer versions — and Ubuntu 24.04 LTS ships tmux 3.4.

So I upgraded the server to tmux 3.5a (built from source) and reran the full test card: attach directly onto the actively-repainting Claude window renders fine, and force-quit-the-app → reopen → reattach comes back with the agent still running. The developer also shipped a sync-output safety net and version-gated a silently-failing request against older tmux in the next build — so both paths to the fix exist. If you’re on a distro that ships tmux 3.4, upgrade before blaming the app.

One config gotcha: aggressive-resize on in your .tmux.conf interferes with control-mode clients (iTerm2 documents the same). Turn it off.

One more layer: use tssh, not plain SSH, as the transport. rootshell ships a mosh-like roaming protocol (tssh; the server side is the open-source trzsz/tsshd, a single binary). It fixes the failure plain SSH can never escape on iPad: iOS suspends the app when you switch away, the TCP connection dies, and you come back to a frozen snapshot. tssh survives suspension and network roaming — and unlike mosh, it’s a true byte stream, so sync-output, OSC 52 clipboard, everything passes through untouched. With tmux underneath, even force-quitting the app costs nothing: reattach, and the agent never noticed you left.

Not affiliated, and it’s early software — but this is the first time the architecture on the iPad has been right. Every touch problem I could blame on rootshell turned out to be a mode-confusion artifact of the old world, and the one genuine bug dissolved under tmux 3.5a — with the developer confirming the mechanism and shipping fixes within a day anyway.

The setup that works

On the server:

# ~/.tmux.conf — the parts that matter
set -g history-limit 100000        # applies to NEW panes only
set -g allow-passthrough on        # Claude Code notifications/progress
set -s extended-keys on            # Shift+Enter in Claude Code
set -as terminal-features 'xterm*:extkeys'
setw -g aggressive-resize off      # ON breaks -CC clients

# start the long-lived session (plus a plain-shell window)
tmux new-session -d -s main -c ~/projects
tmux new-window -d -t main -n static 'exec bash'

Put those two tmux lines in an @reboot cron entry and the session survives reboots without you thinking about it. And check your tmux version: if tmux -V says 3.4 (Ubuntu 24.04’s package), build 3.5a from source — the sync-output handling difference between them is where the only real bug I hit lived.

For the transport, install tsshd on the server (one static binary from trzsz/tsshd into /usr/local/bin) and set the host’s connection type to tssh in rootshell.

On the iPad: install rootshell, add your host (Tailscale IP), connect, and type tmux -CC attach -t main yourself — as of this writing the app’s session popup attaches in regular mode (a control-mode toggle is coming in the next build). The tell that you’re in the right mode: no tmux status bar, windows appear as native tabs. A plain tmux attach silently puts you back in the legacy world.

Keep a plain-SSH fallback in your existing client, and drop this in .bashrc for copying anything longer than a screen (touch selection is a one-screenful tool in every app):

# copy stdin to the iPad clipboard via OSC 52 (works over SSH)
clip() { printf '\033]52;c;%s\a' "$(base64 -w0)"; }
# usage: tail -n 300 build.log | clip

The truth table

| Option | Deep clean scrollback | Native touch selection | Survives disconnect | Verdict | | ------------------------------------ | --------------------- | ---------------------- | ----------------------------- | -------------------------------------- | | Termius/Blink + SSH, no tmux | yes | yes | no — process dies | good fallback | | + mosh | never (protocol) | yes | yes | reading is impossible | | + tmux (mouse mode) | tmux-side only | breaks at screen edge | yes | the standard advice; the standard pain | | Remote Control / mobile apps | n/a — not a terminal | native | yes | approvals only | | rootshell + tssh, no tmux | yes | yes | suspension yes; force-quit no | great until you need immortal sessions | | rootshell + tssh + tmux 3.5a -CC | yes | yes | yes — even force-quit | the answer |

The same session from your iPhone, your Mac, anything

Nothing above is actually iPad-specific. The tmux session on the server is the single source of truth, and any control-mode client attaches to the same live agent:

  • iPhone: rootshell is universal — same tssh host, same tmux -CC attach -t main. On a phone it’s more of a check-in surface than a workspace, and it’s great at that.
  • Mac: iTerm2’s original tmux -CC — the client this whole pattern is borrowed from — attaches to the same session.
  • Desktop Linux/Windows: plain tmux attach works today (with a mouse, the touch problems in this post don’t apply); Ghostty is actively implementing control mode.
  • tsshd allows a second client on the same transport session (--attachable), and tmux has multiplexed multiple clients since forever.

One session, every screen you own. Start a run from the desk, scroll it on the couch, approve the last step from your phone in line at the store.

The part that generalizes

Every dead end in this table failed structurally: mosh’s protocol cannot carry scrollback, tmux’s screen model cannot feed a terminal’s native buffer, a chat mirror cannot become a shell. No configuration, and no amount of app polish, was ever going to fix any of them — and knowing that early is worth more than any workaround, because it tells you to stop tuning and change layers.

And one more lesson I earned the hard way: I spent hours blaming the new app for bugs that belonged to the old stack, because I never verified which mode I was actually measuring. One server-side check (#{client_control_mode}) would have saved the afternoon. Instrument the layer you think you’re testing — or you’re testing a different one.

That’s the same discipline I apply to AI systems for a living: find which layer actually owns the failure before spending money on the wrong one. If your team ships AI features and you suspect you’re tuning prompts to fix an architecture problem, I run that pass as a free reliability teardown — one call, no pitch.

Related Articles

Dig deeper into adjacent topics across RAG, AI security, and platform architecture.