6 minute read

I’ve been looking for a way to run Claude Code sessions remotely—from my phone, while away from my desk.

For simple tasks, the existing options work fine. Claude.ai on mobile handles quick questions. Cursor with Linear integration is decent for focused coding. But I kept hitting walls. Claude’s web interface doesn’t support MCP servers, custom plugins, or hooks. No claude-mem for cross-session memory. No custom skills. No ccstatusline. Basically none of the customizations that make my local Claude Code setup actually productive.

I wanted access to my real environment—the one I’ve spent time configuring—not a stripped-down web version.

Then I read Javier Granda Carvajal’s post on Claude Code on the go. He runs Claude Code on a cloud VM and connects from his phone. I liked the idea but went a different route: my MacBook at home, accessible via mosh and tmux.

The Setup

The architecture is simple:

iPhone (Terminus) → mosh → Mac (tmux) → Claude Code
                              ↓
                         ntfy.sh → phone notification

Terminus is my SSH/mosh client on iOS. It has a touch-friendly keyboard row with common keys like esc, ctrl, tab. Tailscale makes my Mac reachable from anywhere without port forwarding or dynamic DNS. mosh handles intermittent connectivity better than SSH—important when you’re on cellular. tmux keeps sessions alive when I disconnect.

The Problem with Mobile Terminals

Working from a phone screen is cramped. A few issues I ran into:

Scrolling is weird. Native two-finger scroll in Terminus shows the terminal’s scrollback buffer, which mixes content from all tmux windows. Not useful. The fix is using tmux copy mode (Ctrl+a [), but that’s awkward on a virtual keyboard.

Default tmux bindings assume a physical keyboard. Ctrl+b or Ctrl+a followed by [, then Ctrl+u/Ctrl+d to scroll—that’s a lot of modifier key taps on a phone. I added bindings that work better on a touchscreen:

Key Action
Ctrl+a u Enter copy mode, scroll up one page
p / l Half page up / down
o / k Full page up / down

Once in copy mode, single letters only—no modifiers needed.

Here’s the relevant tmux.conf:

# Easy scroll: Ctrl+a u enters copy mode scrolled up
bind u copy-mode -u

# Use vi keys in copy mode
set-window-option -g mode-keys vi

# Copy mode: simple keys for mobile (no modifiers needed)
bind-key -T copy-mode-vi p send-keys -X halfpage-up
bind-key -T copy-mode-vi l send-keys -X halfpage-down
bind-key -T copy-mode-vi o send-keys -X page-up
bind-key -T copy-mode-vi k send-keys -X page-down

One annoyance: I couldn’t find a way to bind Ctrl+a to a single virtual key in Terminus. Would save a tap on every tmux shortcut. If anyone knows how, let me know.

Unicode breaks things. mosh has issues with wide characters. I was getting weird box symbols scattered across the terminal. Turned out my prompt had a Unicode middle dot (·) as a separator. Replaced it with a space. Problem solved.

The Notification Trick

The part I borrowed from Javier’s post: push notifications when Claude needs input.

I’m using ntfy.sh—free, no account needed, dead simple. A curl POST sends a notification to my phone:

curl -d "message" ntfy.sh/my-topic

I set up Claude Code hooks that fire on idle_prompt (Claude waiting 60+ seconds) and permission_prompt (Claude needs approval).

But I didn’t want notifications when I’m sitting at my desk. So the hook checks if the screen is locked first using macOS’s ioreg. Here’s the full hook configuration in ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "ioreg -n Root -d1 -a | grep -q 'CGSSessionScreenIsLocked.*true' && { input=$(cat); dir=$(echo \"$input\" | jq -r .cwd); proj=$(basename \"$dir\"); branch=$(git -C \"$dir\" branch --show-current 2>/dev/null || echo '-'); sess=$(echo \"$input\" | ~/bin/claude-session-name); curl -s -H \"Title: Claude: Waiting [$sess]\" -d \"$proj ($branch)\" ntfy.sh/your-secret-topic; }"
          }
        ]
      },
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "ioreg -n Root -d1 -a | grep -q 'CGSSessionScreenIsLocked.*true' && { input=$(cat); dir=$(echo \"$input\" | jq -r .cwd); proj=$(basename \"$dir\"); branch=$(git -C \"$dir\" branch --show-current 2>/dev/null || echo '-'); sess=$(echo \"$input\" | ~/bin/claude-session-name); curl -s -H \"Title: Claude: Permission [$sess]\" -d \"$proj ($branch)\" ntfy.sh/your-secret-topic; }"
          }
        ]
      }
    ]
  }
}

The hook reads JSON from stdin (provided by Claude Code), extracts the working directory and session ID, generates a memorable name, and sends the notification. Only fires when the Mac screen is locked.

Session Names

Here’s a small addition I’m happy with: each Claude session gets a memorable name like dry-fox or shy-cod.

The problem: when you have multiple Claude sessions running, notifications that say “Claude needs input” aren’t helpful. Which session? The hook has access to session_id, so I hash it to pick words from two lists.

Here’s the full script (~/bin/claude-session-name):

#!/bin/bash
# Generate memorable session name from Claude session_id
# Usage: echo '{"session_id":"abc123"}' | claude-session-name

adj=(red big sly old icy dry hot shy mad sad odd raw)
ani=(fox cat owl bee bat ant eel cod hen ram yak ape)

sid=$(jq -r '.session_id // empty')
[[ -z "$sid" ]] && exit 1

h=$(echo -n "$sid" | md5 | cut -c1-4)
i=$((16#${h:0:2} % 12))
j=$((16#${h:2:2} % 12))

echo "${adj[$i]}-${ani[$j]}"

144 combinations, all under 10 characters. Same session always gets the same name. The script reads JSON from stdin, hashes the session ID, and uses the hash to index into the word arrays.

Now notifications show:

  • Title: Claude: Waiting [dry-fox]
  • Body: my-project (feature-branch)

Some real examples:

Title Body What happened
Claude: Waiting [shy-owl] api-service (main) Claude finished a task and needs direction
Claude: Permission [red-bat] webapp (feature-auth) Claude wants to run npm test
Claude: Waiting [icy-fox] infra (terraform-refactor) Claude has a question about implementation

I can glance at my phone and know exactly which session needs attention.

I also added this to ccstatusline as a custom command widget, so the session name shows in the terminal status bar too.

How I Actually Use This

Let’s be honest about the downsides first.

Terminal width on an iPhone Pro Max is around 50-60 characters. Claude’s output wraps awkwardly. The virtual keyboard eats half the screen, leaving maybe 10-15 lines visible. Reading long responses means constant scrolling. This isn’t a replacement for working at a desk.

But here’s how it fits into my workflow.

I start Claude sessions on my MacBook, usually in iTerm2 which has excellent tmux integration—tmux panes become iTerm tabs, native scrolling works, and Cmd+K clears the buffer properly. It’s the best of both worlds: tmux persistence with a native terminal feel.

The real work happens at my desk. Brainstorming, designing features, getting Claude to spec things out. Once the direction is clear, I kick off a long-running skill that orchestrates subagents—implementing code across multiple files, running reviews, linting, typechecking, the whole pipeline. Automation does the heavy lifting.

A good session can run for an hour or more.

Meanwhile, I’m downstairs drinking tea. Or working out. Or running errands. When Claude needs a decision or hits a permission prompt, my phone buzzes. I glance at the notification—Claude: Waiting [shy-owl]—pull up Terminus, answer the question, and go back to whatever I was doing.

That’s the actual value: not coding on my phone, but staying connected to automated work happening elsewhere.

I outlined this blog post from my phone and asked Claude to polish it—using the same setup described here. Meta, but it worked.

Cheers.

Updated:

Comments