Skip to content

Instantly share code, notes, and snippets.

@thomasht86
Last active January 3, 2026 17:12
Show Gist options
  • Select an option

  • Save thomasht86/86f0f8f62db1839054abd8a7e501ff7d to your computer and use it in GitHub Desktop.

Select an option

Save thomasht86/86f0f8f62db1839054abd8a7e501ff7d to your computer and use it in GitHub Desktop.
Claude code on mobile
#!/bin/bash
#
# Mobile Claude - One-command setup for running Claude Code remotely
# Supports: Arch, Ubuntu/Debian, Fedora, macOS
#
# Usage: curl -fsSL https://gist.github.com/thomasht86/86f0f8f62db1839054abd8a7e501ff7d/raw/935bbfa0957cd5926751742189441cf10fbe2ba0/setup.sh | bash
#
set -e
# ============================================================================
# CONFIGURATION
# ============================================================================
INSTALL_DIR="/usr/local/bin"
LAUNCHER_NAME="mobile-claude"
VERSION="1.0.0"
# ============================================================================
# COLORS & OUTPUT
# ============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m' # No Color
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
header() { echo -e "\n${BOLD}=== $1 ===${NC}\n"; }
# ============================================================================
# HELP & VERSION
# ============================================================================
show_help() {
cat << EOF
Mobile Claude Setup v${VERSION}
Run Claude Code remotely from your mobile device.
USAGE:
./setup.sh [OPTIONS]
OPTIONS:
--help, -h Show this help message
--check Check if dependencies are installed (no install)
--uninstall Remove mobile-claude and dependencies
--version, -v Show version
QUICK START:
curl -fsSL <gist-url>/setup.sh | bash
WHAT IT INSTALLS:
- ttyd Web-based terminal
- tailscale Secure VPN tunnel
- tmux Terminal multiplexer
- qrencode QR code generator
- npm Node.js package manager
- claude-code Anthropic's CLI for Claude
AFTER INSTALL:
Run 'mobile-claude' to start your session.
EOF
exit 0
}
show_version() {
echo "Mobile Claude Setup v${VERSION}"
exit 0
}
# ============================================================================
# OS & PACKAGE MANAGER DETECTION
# ============================================================================
detect_os() {
if [[ "$OSTYPE" == "darwin"* ]]; then
OS="macos"
elif [[ -f /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
arch|manjaro|endeavouros) OS="arch" ;;
ubuntu|debian|pop|linuxmint) OS="debian" ;;
fedora|rhel|centos|rocky|alma) OS="fedora" ;;
alpine) OS="alpine" ;;
*)
# Try ID_LIKE for derivatives
case "$ID_LIKE" in
*arch*) OS="arch" ;;
*debian*|*ubuntu*) OS="debian" ;;
*fedora*|*rhel*) OS="fedora" ;;
*) OS="unknown" ;;
esac
;;
esac
else
OS="unknown"
fi
echo "$OS"
}
detect_package_manager() {
case "$1" in
macos) echo "brew" ;;
arch) echo "pacman" ;;
debian) echo "apt" ;;
fedora) echo "dnf" ;;
alpine) echo "apk" ;;
*) echo "unknown" ;;
esac
}
# ============================================================================
# DEPENDENCY INSTALLATION
# ============================================================================
install_deps_pacman() {
info "Installing dependencies via pacman..."
sudo pacman -Syu --noconfirm ttyd tailscale tmux qrencode npm
}
install_deps_apt() {
info "Installing dependencies via apt..."
# Add Tailscale repo
if ! command -v tailscale &> /dev/null; then
info "Adding Tailscale repository..."
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list
fi
sudo apt update
sudo apt install -y tmux qrencode npm
# ttyd may not be in default repos
if ! command -v ttyd &> /dev/null; then
if apt-cache show ttyd &> /dev/null; then
sudo apt install -y ttyd
else
warn "ttyd not in repos. Installing from GitHub release..."
install_ttyd_binary
fi
fi
sudo apt install -y tailscale
}
install_deps_dnf() {
info "Installing dependencies via dnf..."
sudo dnf install -y tmux qrencode npm
# Add Tailscale repo for Fedora
if ! command -v tailscale &> /dev/null; then
info "Adding Tailscale repository..."
sudo dnf config-manager --add-repo https://pkgs.tailscale.com/stable/fedora/tailscale.repo
sudo dnf install -y tailscale
fi
# ttyd may need COPR
if ! command -v ttyd &> /dev/null; then
if dnf list ttyd &> /dev/null 2>&1; then
sudo dnf install -y ttyd
else
warn "ttyd not in repos. Installing from GitHub release..."
install_ttyd_binary
fi
fi
}
install_deps_brew() {
info "Installing dependencies via Homebrew..."
if ! command -v brew &> /dev/null; then
error "Homebrew not found. Install it first: https://brew.sh"
exit 1
fi
brew install ttyd tailscale tmux qrencode node
}
install_deps_apk() {
info "Installing dependencies via apk..."
sudo apk add --no-cache ttyd tailscale tmux qrencode npm
}
install_ttyd_binary() {
# Fallback: download ttyd binary from GitHub
local arch=$(uname -m)
local ttyd_arch
case "$arch" in
x86_64) ttyd_arch="x86_64" ;;
aarch64|arm64) ttyd_arch="aarch64" ;;
armv7l) ttyd_arch="armhf" ;;
*) error "Unsupported architecture: $arch"; exit 1 ;;
esac
info "Downloading ttyd for $ttyd_arch..."
local url="https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${ttyd_arch}"
sudo curl -fsSL "$url" -o /usr/local/bin/ttyd
sudo chmod +x /usr/local/bin/ttyd
success "ttyd installed from GitHub"
}
# ============================================================================
# CLAUDE CODE INSTALLATION
# ============================================================================
install_claude_code() {
if command -v claude &> /dev/null; then
success "Claude Code CLI already installed"
return
fi
info "Installing Claude Code CLI via npm..."
sudo npm install -g @anthropic-ai/claude-code
if command -v claude &> /dev/null; then
success "Claude Code CLI installed"
else
error "Failed to install Claude Code CLI"
exit 1
fi
}
# ============================================================================
# SERVICE SETUP
# ============================================================================
start_tailscale_service() {
local os=$1
if [[ "$os" == "macos" ]]; then
# macOS: Tailscale can run as GUI app or CLI daemon
if ! pgrep -x "Tailscale" > /dev/null && ! pgrep -x "tailscaled" > /dev/null; then
info "Starting Tailscale daemon..."
# Try to start the GUI app first (if installed)
if [[ -d "/Applications/Tailscale.app" ]]; then
info "Opening Tailscale.app..."
open -a Tailscale
sleep 2
elif [[ -d "$HOME/Applications/Tailscale.app" ]]; then
info "Opening Tailscale.app from ~/Applications..."
open -a "$HOME/Applications/Tailscale.app"
sleep 2
else
# Headless/daemon mode via Homebrew formula
# Must use sudo for system service
info "Starting Tailscale as system service..."
sudo brew services start tailscale
sleep 2
fi
else
success "Tailscale is already running"
fi
# Verify it's running
sleep 1
if ! pgrep -x "Tailscale" > /dev/null && ! pgrep -x "tailscaled" > /dev/null; then
warn "Tailscale daemon may not be running"
echo ""
echo "Try running manually:"
echo " sudo brew services start tailscale"
echo " tailscale up"
echo ""
fi
else
# Linux: systemd
if command -v systemctl &> /dev/null; then
info "Enabling Tailscale service..."
sudo systemctl enable --now tailscaled 2>/dev/null || true
fi
fi
# Now authenticate with Tailscale if not already connected
authenticate_tailscale
}
authenticate_tailscale() {
# Check if already authenticated
local status=$(tailscale status --json 2>/dev/null | grep -o '"BackendState"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
if [[ "$status" == "Running" ]]; then
success "Tailscale is already authenticated"
return
fi
info "Authenticating with Tailscale..."
echo ""
echo "A browser window will open for you to log in to Tailscale."
echo "If it doesn't open automatically, copy the URL shown below."
echo ""
# Run tailscale up - this will print a URL or open browser
tailscale up
# Verify authentication succeeded
sleep 2
status=$(tailscale status --json 2>/dev/null | grep -o '"BackendState"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
if [[ "$status" == "Running" ]]; then
success "Tailscale authenticated successfully"
else
warn "Tailscale authentication may not have completed"
echo "You can try again later with: tailscale up"
fi
}
# ============================================================================
# LAUNCHER SCRIPT (EMBEDDED)
# ============================================================================
create_launcher() {
info "Installing mobile-claude launcher to ${INSTALL_DIR}..."
sudo tee "${INSTALL_DIR}/${LAUNCHER_NAME}" > /dev/null << 'LAUNCHER_EOF'
#!/bin/bash
#
# Mobile Claude - Run Claude Code from your mobile device
#
set -e
# ============================================================================
# CONFIGURATION
# ============================================================================
WEB_USER="claude"
WEB_PASS="code"
PORT=7681
SESSION_NAME="mobile-claude"
# ============================================================================
# COLORS
# ============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ============================================================================
# HELP
# ============================================================================
show_help() {
cat << EOF
Mobile Claude - Run Claude Code remotely
USAGE:
mobile-claude [OPTIONS]
OPTIONS:
--help, -h Show this help
--status Show current session status
--stop Stop running session
--port PORT Use custom port (default: 7681)
CREDENTIALS:
Username: ${WEB_USER}
Password: ${WEB_PASS}
EOF
exit 0
}
# ============================================================================
# STATUS & STOP
# ============================================================================
show_status() {
echo -e "${BOLD}Mobile Claude Status${NC}"
echo "-------------------"
# Check tmux session
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
success "Tmux session '$SESSION_NAME' is running"
else
warn "Tmux session '$SESSION_NAME' is not running"
fi
# Check ttyd
if pgrep -f "ttyd.*$PORT" > /dev/null; then
success "ttyd is running on port $PORT"
else
warn "ttyd is not running"
fi
# Check Tailscale
if command -v tailscale &> /dev/null; then
local ts_status=$(tailscale status --json 2>/dev/null | grep -o '"BackendState"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
if [[ "$ts_status" == "Running" ]]; then
local ts_host=$(tailscale status --json | grep -o '"DNSName"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n 1 | sed 's/.*"\([^"]*\)"$/\1/')
ts_host=${ts_host%.}
success "Tailscale connected: https://${ts_host}"
else
warn "Tailscale not connected (state: $ts_status)"
fi
fi
exit 0
}
stop_session() {
info "Stopping Mobile Claude..."
# Kill ttyd
if pkill -f "ttyd.*$PORT" 2>/dev/null; then
success "Stopped ttyd"
fi
# Kill tmux session
if tmux kill-session -t "$SESSION_NAME" 2>/dev/null; then
success "Stopped tmux session"
fi
# Remove Tailscale serve
if command -v tailscale &> /dev/null; then
tailscale serve --https=443 off 2>/dev/null || true
success "Removed Tailscale serve"
fi
success "Mobile Claude stopped"
exit 0
}
# ============================================================================
# DEPENDENCY CHECKS
# ============================================================================
check_deps() {
local missing=()
for cmd in tailscale ttyd tmux qrencode claude; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
error "Missing dependencies: ${missing[*]}"
echo "Run the setup script to install them."
exit 1
fi
}
check_port() {
if lsof -i ":$PORT" &> /dev/null; then
error "Port $PORT is already in use"
echo "Use --port to specify a different port, or --stop to kill existing session"
exit 1
fi
}
# ============================================================================
# TAILSCALE SETUP
# ============================================================================
setup_tailscale() {
local status=$(tailscale status --json 2>/dev/null | grep -o '"BackendState"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
if [[ "$status" != "Running" ]]; then
warn "Tailscale is not connected"
info "Please run: tailscale up"
echo ""
echo "After logging in, run this script again."
exit 1
fi
# Get Tailscale hostname
TS_HOST=$(tailscale status --json | grep -o '"DNSName"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n 1 | sed 's/.*"\([^"]*\)"$/\1/')
TS_HOST=${TS_HOST%.}
if [[ -z "$TS_HOST" ]]; then
error "Could not determine Tailscale hostname"
exit 1
fi
FULL_URL="https://${TS_HOST}"
}
# ============================================================================
# MAIN
# ============================================================================
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h) show_help ;;
--status) show_status ;;
--stop) stop_session ;;
--port)
PORT="$2"
shift 2
;;
*)
error "Unknown option: $1"
show_help
;;
esac
done
# Pre-flight checks
check_deps
check_port
setup_tailscale
# Banner
echo ""
echo -e "${BOLD}=======================================================${NC}"
echo -e "${BOLD} Mobile Claude - Claude Code on Mobile ${NC}"
echo -e "${BOLD}=======================================================${NC}"
echo ""
# Configure Tailscale Serve
info "Setting up secure tunnel..."
sudo tailscale serve --bg --https=443 localhost:$PORT 2>/dev/null || {
# Try without sudo on macOS
tailscale serve --bg --https=443 localhost:$PORT 2>/dev/null || {
error "Failed to configure Tailscale serve"
exit 1
}
}
success "Tunnel configured"
# Display QR code
echo ""
echo -e "${BOLD}Scan to connect:${NC}"
echo ""
qrencode -t ANSIUTF8 "$FULL_URL"
echo ""
echo -e "${BOLD}URL:${NC} $FULL_URL"
echo -e "${BOLD}Username:${NC} $WEB_USER"
echo -e "${BOLD}Password:${NC} $WEB_PASS"
echo ""
echo -e "${BOLD}=======================================================${NC}"
echo -e "Press ${BOLD}Ctrl+C${NC} to stop the server"
echo -e "${BOLD}=======================================================${NC}"
echo ""
# Trap for clean shutdown
cleanup() {
echo ""
info "Shutting down..."
tailscale serve --https=443 off 2>/dev/null || true
exit 0
}
trap cleanup SIGINT SIGTERM
# Start ttyd with tmux
info "Starting terminal server..."
ttyd -W -c "$WEB_USER:$WEB_PASS" -p "$PORT" \
-t disableFlowControl=true \
-t fontSize=16 \
tmux new-session -A -s "$SESSION_NAME" "claude"
LAUNCHER_EOF
sudo chmod +x "${INSTALL_DIR}/${LAUNCHER_NAME}"
success "Launcher installed at ${INSTALL_DIR}/${LAUNCHER_NAME}"
}
# ============================================================================
# VERIFICATION
# ============================================================================
verify_installation() {
header "Verifying Installation"
local failed=0
for cmd in ttyd tailscale tmux qrencode npm claude; do
if command -v "$cmd" &> /dev/null; then
success "$cmd installed"
else
error "$cmd NOT found"
failed=1
fi
done
if [[ -x "${INSTALL_DIR}/${LAUNCHER_NAME}" ]]; then
success "mobile-claude launcher installed"
else
error "mobile-claude launcher NOT found"
failed=1
fi
return $failed
}
# ============================================================================
# CHECK MODE
# ============================================================================
check_only() {
header "Checking Dependencies"
for cmd in ttyd tailscale tmux qrencode npm claude; do
if command -v "$cmd" &> /dev/null; then
success "$cmd: $(command -v $cmd)"
else
warn "$cmd: NOT installed"
fi
done
if [[ -x "${INSTALL_DIR}/${LAUNCHER_NAME}" ]]; then
success "mobile-claude launcher: ${INSTALL_DIR}/${LAUNCHER_NAME}"
else
warn "mobile-claude launcher: NOT installed"
fi
exit 0
}
# ============================================================================
# UNINSTALL
# ============================================================================
uninstall() {
header "Uninstalling Mobile Claude"
warn "This will remove mobile-claude but NOT system packages."
read -p "Continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
# Remove launcher
if [[ -f "${INSTALL_DIR}/${LAUNCHER_NAME}" ]]; then
sudo rm "${INSTALL_DIR}/${LAUNCHER_NAME}"
success "Removed ${INSTALL_DIR}/${LAUNCHER_NAME}"
fi
# Remove Claude Code CLI
if command -v claude &> /dev/null; then
read -p "Remove Claude Code CLI? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
sudo npm uninstall -g @anthropic-ai/claude-code
success "Removed Claude Code CLI"
fi
fi
success "Uninstall complete"
echo ""
echo "System packages (ttyd, tailscale, etc.) were NOT removed."
echo "Remove them manually with your package manager if needed."
exit 0
}
# ============================================================================
# MAIN
# ============================================================================
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h) show_help ;;
--version|-v) show_version ;;
--check) check_only ;;
--uninstall) uninstall ;;
*)
error "Unknown option: $1"
show_help
;;
esac
shift
done
# Banner
echo ""
echo -e "${BOLD}=======================================================${NC}"
echo -e "${BOLD} Mobile Claude Setup v${VERSION} ${NC}"
echo -e "${BOLD}=======================================================${NC}"
echo ""
# Detect OS
header "Detecting System"
OS=$(detect_os)
PKG_MGR=$(detect_package_manager "$OS")
if [[ "$OS" == "unknown" || "$PKG_MGR" == "unknown" ]]; then
error "Unsupported operating system"
echo "Supported: Arch, Ubuntu/Debian, Fedora, macOS"
exit 1
fi
info "OS: $OS"
info "Package manager: $PKG_MGR"
# Install dependencies
header "Installing Dependencies"
case "$PKG_MGR" in
pacman) install_deps_pacman ;;
apt) install_deps_apt ;;
dnf) install_deps_dnf ;;
brew) install_deps_brew ;;
apk) install_deps_apk ;;
esac
# Install Claude Code
header "Installing Claude Code CLI"
install_claude_code
# Start services
header "Configuring Services"
start_tailscale_service "$OS"
# Create launcher
header "Installing Launcher"
create_launcher
# Verify
if verify_installation; then
echo ""
echo -e "${GREEN}${BOLD}=======================================================${NC}"
echo -e "${GREEN}${BOLD} Setup Complete! ${NC}"
echo -e "${GREEN}${BOLD}=======================================================${NC}"
echo ""
echo "Next steps:"
echo ""
echo " 1. Start Mobile Claude:"
echo -e " ${BOLD}mobile-claude${NC}"
echo ""
echo " 2. Scan the QR code on your mobile device"
echo ""
else
echo ""
error "Installation incomplete. Check errors above."
exit 1
fi
}
main "$@"
@thomasht86
Copy link
Author

To run

curl -fsSL https://gist.github.com/thomasht86/86f0f8f62db1839054abd8a7e501ff7d/raw/935bbfa0957cd5926751742189441cf10fbe2ba0/setup.sh | bash

@thomasht86
Copy link
Author

Tested on MacOS and Arch linux.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment