feat: setup inicial para Claude Code en Termux
Scripts, configuraciones y documentación para ejecutar Claude Code CLI en Termux/Android. Incluye wrapper con termux-chroot para /tmp, actualizador seguro con techo de versión v2.1.112, resolver mDNS puro en Python y ProxyCommand SSH con fallback Tailscale. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
0d5d099dee
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Claude Code for Termux
|
||||||
|
|
||||||
|
Run [Claude Code](https://claude.ai/code) CLI on Android via Termux.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Claude Code v2.1.113+ switched to native binaries, breaking Termux/Android compatibility. This repo provides:
|
||||||
|
|
||||||
|
- **v2.1.112** (last pure Node.js release) with auto-update protection
|
||||||
|
- **termux-chroot wrapper** that fixes the `/tmp` permission issue
|
||||||
|
- **mDNS resolver** for SSH without hardcoded IPs
|
||||||
|
- **Safe updater** that respects the version ceiling
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [Termux](https://f-droid.org/en/packages/com.termux/) (F-Droid recommended)
|
||||||
|
- Android 10+ (ARM64)
|
||||||
|
- ~500MB free storage
|
||||||
|
- An Anthropic API key or Claude subscription
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <your-gitea-url>/andres/claude-code-termux.git
|
||||||
|
cd claude-code-termux
|
||||||
|
bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or dry-run first:
|
||||||
|
```bash
|
||||||
|
bash install.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Installed
|
||||||
|
|
||||||
|
| Component | Location | Purpose |
|
||||||
|
|-----------|----------|---------|
|
||||||
|
| Claude Code v2.1.112 | npm global | The CLI itself |
|
||||||
|
| `clauded` | `~/.local/bin/` | Wrapper with termux-chroot for /tmp |
|
||||||
|
| `claude-check-env` | `~/.local/bin/` | Environment validator |
|
||||||
|
| `claude-update` | `~/.local/bin/` | Safe updater (ceiling-aware) |
|
||||||
|
| `mdns-resolve` | `~/.local/bin/` | Pure Python mDNS resolver |
|
||||||
|
| `ssh-mdns-proxy` | `~/.local/bin/` | SSH ProxyCommand with mDNS+Tailscale |
|
||||||
|
| `settings.json` | `~/.claude/` | Disables auto-updater |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start Claude Code (always use this, not `claude` directly)
|
||||||
|
clauded
|
||||||
|
|
||||||
|
# Check environment
|
||||||
|
claude-check-env
|
||||||
|
|
||||||
|
# Update (safe, respects version ceiling)
|
||||||
|
claude-update
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSH with Dynamic Resolution
|
||||||
|
|
||||||
|
The included `ssh-mdns-proxy` resolves hostnames via mDNS first, then Tailscale DNS, then cache. See `config/ssh_config.example` for setup.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set your Tailscale domain (optional, has a default)
|
||||||
|
export TAILSCALE_DOMAIN="your-tailnet.ts.net"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Ceiling
|
||||||
|
|
||||||
|
**v2.1.112 is the last compatible version.** Starting v2.1.113, Claude Code ships native binaries requiring glibc/musl linkers that don't exist on Android (Bionic libc).
|
||||||
|
|
||||||
|
`claude-update` enforces this automatically. See `docs/TROUBLESHOOTING.md` for details.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for:
|
||||||
|
- `/tmp` permission errors
|
||||||
|
- Native binary errors
|
||||||
|
- Auto-updater issues
|
||||||
|
- SSH resolution problems
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# claude-check-env: Verify Claude Code environment on Termux
|
||||||
|
#
|
||||||
|
|
||||||
|
RED="\033[0;31m"
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
YELLOW="\033[1;33m"
|
||||||
|
CYAN="\033[0;36m"
|
||||||
|
NC="\033[0m"
|
||||||
|
|
||||||
|
echo -e "${CYAN}=== Claude Code - Environment Check ===${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Claude version
|
||||||
|
CLAUDE_VER=$(claude --version 2>/dev/null | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1)
|
||||||
|
if [[ -n "$CLAUDE_VER" ]]; then
|
||||||
|
echo -e "${GREEN}[OK]${NC} Claude Code: v$CLAUDE_VER"
|
||||||
|
else
|
||||||
|
echo -e "${RED}[ERROR]${NC} Claude Code not installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Node.js
|
||||||
|
NODE_VER=$(node --version 2>/dev/null)
|
||||||
|
if [[ -n "$NODE_VER" ]]; then
|
||||||
|
echo -e "${GREEN}[OK]${NC} Node.js: $NODE_VER"
|
||||||
|
else
|
||||||
|
echo -e "${RED}[ERROR]${NC} Node.js not installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. proot/termux-chroot
|
||||||
|
if command -v termux-chroot &>/dev/null; then
|
||||||
|
echo -e "${GREEN}[OK]${NC} termux-chroot available"
|
||||||
|
else
|
||||||
|
echo -e "${RED}[ERROR]${NC} termux-chroot not available (pkg install proot)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Sharp WASM (image support)
|
||||||
|
SHARP_WASM=$(npm list -g @img/sharp-wasm32 2>/dev/null | grep sharp-wasm32)
|
||||||
|
if [[ -n "$SHARP_WASM" ]]; then
|
||||||
|
echo -e "${GREEN}[OK]${NC} Sharp WASM: installed (image support)"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} Sharp WASM not installed (no image support)"
|
||||||
|
echo " Install: npm install -g @img/sharp-wasm32 sharp --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Credentials
|
||||||
|
if [[ -f ~/.claude/.credentials.json ]]; then
|
||||||
|
echo -e "${GREEN}[OK]${NC} Credentials: configured"
|
||||||
|
else
|
||||||
|
echo -e "${RED}[ERROR]${NC} No credentials (run: claude auth)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. TMPDIR
|
||||||
|
echo -e "${GREEN}[OK]${NC} TMPDIR: ${TMPDIR:-not set}"
|
||||||
|
|
||||||
|
# 7. Tailscale (generic check via interface)
|
||||||
|
if ip link show tailscale0 &>/dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}[OK]${NC} Tailscale: connected"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} Tailscale: not connected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 8. Disk space
|
||||||
|
SPACE=$(df -h "$PREFIX" 2>/dev/null | tail -1 | awk '{print $4}')
|
||||||
|
echo -e "${GREEN}[OK]${NC} Free space: $SPACE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}=== Available Commands ===${NC}"
|
||||||
|
echo " clauded - Start Claude (interactive mode)"
|
||||||
|
echo " claude-update - Update Claude Code (safe)"
|
||||||
|
echo " claude-check-env - This check"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}=== Known Limitations ===${NC}"
|
||||||
|
echo " - Background tasks may fail (use termux-chroot via clauded)"
|
||||||
|
echo " - Version ceiling: v2.1.112 (last pure Node.js release)"
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# claude-update: Safe updater for Claude Code on Termux
|
||||||
|
# Respects version ceiling (v2.1.112 = last pure Node.js release)
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED="\033[0;31m"
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
YELLOW="\033[1;33m"
|
||||||
|
NC="\033[0m"
|
||||||
|
|
||||||
|
MAX_VERSION="2.1.112"
|
||||||
|
|
||||||
|
CURRENT=$(claude --version 2>/dev/null | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1)
|
||||||
|
echo "Current: v${CURRENT:-not installed}"
|
||||||
|
|
||||||
|
# Check what latest available is
|
||||||
|
LATEST=$(npm view @anthropic-ai/claude-code version 2>/dev/null)
|
||||||
|
echo "Latest available: v${LATEST:-unknown}"
|
||||||
|
echo "Termux ceiling: v$MAX_VERSION"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Compare versions (is latest > ceiling?)
|
||||||
|
version_gt() {
|
||||||
|
test "$(printf '%s\n' "$1" "$2" | sort -V | tail -1)" != "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$CURRENT" == "$MAX_VERSION" ]]; then
|
||||||
|
echo -e "${GREEN}Already at ceiling version v$MAX_VERSION${NC}"
|
||||||
|
echo "Versions above this use native binaries (incompatible with Termux/Android)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if version_gt "$LATEST" "$MAX_VERSION"; then
|
||||||
|
echo -e "${YELLOW}Latest (v$LATEST) exceeds ceiling.${NC}"
|
||||||
|
echo "Installing ceiling version v$MAX_VERSION instead..."
|
||||||
|
TARGET="$MAX_VERSION"
|
||||||
|
else
|
||||||
|
TARGET="$LATEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify target is pure Node.js (has cli.js, not cli-wrapper.cjs)
|
||||||
|
echo "Verifying v$TARGET is Node.js compatible..."
|
||||||
|
FILES=$(npm pack "@anthropic-ai/claude-code@$TARGET" --dry-run 2>&1 || true)
|
||||||
|
if echo "$FILES" | grep -q "cli-wrapper.cjs"; then
|
||||||
|
echo -e "${RED}ERROR: v$TARGET uses native binaries (cli-wrapper.cjs)${NC}"
|
||||||
|
echo "This version is NOT compatible with Termux/Android."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unprotect if previously locked
|
||||||
|
CLAUDE_DIR="$PREFIX/lib/node_modules/@anthropic-ai/claude-code"
|
||||||
|
if [[ -d "$CLAUDE_DIR" ]]; then
|
||||||
|
chmod -R u+w "$CLAUDE_DIR" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing v$TARGET..."
|
||||||
|
npm install -g "@anthropic-ai/claude-code@$TARGET"
|
||||||
|
|
||||||
|
# Re-protect against auto-updater
|
||||||
|
chmod -R a-w "$CLAUDE_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
|
NEW=$(claude --version 2>/dev/null | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1)
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}Updated: v$CURRENT -> v$NEW${NC}"
|
||||||
|
echo "Package protected against auto-updater (chmod a-w)"
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# clauded: Wrapper for Claude Code on Termux
|
||||||
|
# Uses termux-chroot for working /tmp in all child processes
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TERMUX_TMP="$PREFIX/tmp"
|
||||||
|
|
||||||
|
RED="\033[0;31m"
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
YELLOW="\033[1;33m"
|
||||||
|
NC="\033[0m"
|
||||||
|
|
||||||
|
# Verify termux-chroot
|
||||||
|
if ! command -v termux-chroot &>/dev/null; then
|
||||||
|
echo -e "${RED}Error: termux-chroot not available${NC}"
|
||||||
|
echo "Install with: pkg install proot"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify claude
|
||||||
|
if ! command -v claude &>/dev/null; then
|
||||||
|
echo -e "${RED}Error: claude is not installed${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean orphan Claude temp files (older than 1 day)
|
||||||
|
find "$TERMUX_TMP" -name "claude-*-cwd" -mtime +1 -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# Check Tailscale connectivity (generic: look for tailscale0 interface)
|
||||||
|
if ! ip link show tailscale0 &>/dev/null 2>&1; then
|
||||||
|
echo -e "${YELLOW}Tailscale not connected${NC} - SSH to remote hosts will fail"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
CLAUDE_VER=$(claude --version 2>/dev/null | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1)
|
||||||
|
echo -e "${GREEN}Claude Code v$CLAUDE_VER (termux-chroot)${NC}"
|
||||||
|
echo -e " /tmp -> /usr/tmp (FHS)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exec termux-chroot claude --dangerously-skip-permissions "$@"
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Resolve hostname.local via mDNS multicast (pure Python, no dependencies).
|
||||||
|
Usage: mdns-resolve <hostname.local> [timeout_seconds]
|
||||||
|
Returns: IP address on stdout, or exit 1 on failure.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import select
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def build_mdns_query(hostname: str) -> bytes:
|
||||||
|
header = struct.pack('>HHHHHH', 0, 0, 1, 0, 0, 0)
|
||||||
|
question = b''
|
||||||
|
for part in hostname.rstrip('.').split('.'):
|
||||||
|
question += bytes([len(part)]) + part.encode()
|
||||||
|
question += b'\x00'
|
||||||
|
question += struct.pack('>HH', 1, 1) # Type A, Class IN
|
||||||
|
return header + question
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mdns_response(data: bytes) -> str | None:
|
||||||
|
if len(data) < 12:
|
||||||
|
return None
|
||||||
|
qdcount = struct.unpack('>H', data[4:6])[0]
|
||||||
|
ancount = struct.unpack('>H', data[6:8])[0]
|
||||||
|
if ancount == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset = 12
|
||||||
|
for _ in range(qdcount):
|
||||||
|
while offset < len(data) and data[offset] != 0:
|
||||||
|
if data[offset] & 0xc0 == 0xc0:
|
||||||
|
offset += 2
|
||||||
|
break
|
||||||
|
offset += data[offset] + 1
|
||||||
|
else:
|
||||||
|
offset += 1
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
for _ in range(ancount):
|
||||||
|
while offset < len(data):
|
||||||
|
if data[offset] & 0xc0 == 0xc0:
|
||||||
|
offset += 2
|
||||||
|
break
|
||||||
|
elif data[offset] == 0:
|
||||||
|
offset += 1
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
offset += data[offset] + 1
|
||||||
|
if offset + 10 > len(data):
|
||||||
|
break
|
||||||
|
rtype, rclass, ttl, rdlength = struct.unpack('>HHIH', data[offset:offset+10])
|
||||||
|
offset += 10
|
||||||
|
if rtype == 1 and rdlength == 4 and offset + 4 <= len(data):
|
||||||
|
return socket.inet_ntoa(data[offset:offset+4])
|
||||||
|
offset += rdlength
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_mdns(hostname: str, timeout: float = 2.0) -> str | None:
|
||||||
|
if not hostname.endswith('.local'):
|
||||||
|
hostname += '.local'
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.setblocking(False)
|
||||||
|
sock.sendto(build_mdns_query(hostname), ('224.0.0.251', 5353))
|
||||||
|
end_time = time.time() + timeout
|
||||||
|
while time.time() < end_time:
|
||||||
|
ready, _, _ = select.select([sock], [], [], 0.1)
|
||||||
|
if ready:
|
||||||
|
try:
|
||||||
|
data, _ = sock.recvfrom(4096)
|
||||||
|
ip = parse_mdns_response(data)
|
||||||
|
if ip:
|
||||||
|
sock.close()
|
||||||
|
return ip
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: mdns-resolve <hostname.local> [timeout]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
hostname = sys.argv[1]
|
||||||
|
timeout = float(sys.argv[2]) if len(sys.argv) > 2 else 2.0
|
||||||
|
ip = resolve_mdns(hostname, timeout)
|
||||||
|
if ip:
|
||||||
|
print(ip)
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ssh-mdns-proxy: Resolve .local via mDNS with fallbacks, then connect
|
||||||
|
#
|
||||||
|
# Resolution order: mDNS → Tailscale DNS → cached IP
|
||||||
|
# Usage: ssh-mdns-proxy <hostname> [port]
|
||||||
|
#
|
||||||
|
# Configure your Tailscale domain:
|
||||||
|
TAILSCALE_DOMAIN="${TAILSCALE_DOMAIN:-tailb0bb74.ts.net}"
|
||||||
|
|
||||||
|
HOST="$1"
|
||||||
|
PORT="${2:-22}"
|
||||||
|
CACHE="$HOME/.ssh/mdns-cache"
|
||||||
|
QUERY="${HOST%.local}"
|
||||||
|
|
||||||
|
# Alias mapping (short name -> mDNS hostname)
|
||||||
|
declare -A ALIASES=(
|
||||||
|
[dell]="dell-latitude3400"
|
||||||
|
[lenovo]="lenovo-ideapad"
|
||||||
|
)
|
||||||
|
[[ -n "${ALIASES[$QUERY]}" ]] && QUERY="${ALIASES[$QUERY]}"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# 1. mDNS resolution
|
||||||
|
IP=$("$SCRIPT_DIR/mdns-resolve" "${QUERY}.local" 2 2>/dev/null)
|
||||||
|
|
||||||
|
# 2. Fallback: Tailscale DNS
|
||||||
|
if [[ -z "$IP" ]]; then
|
||||||
|
IP=$(getent hosts "${QUERY}.${TAILSCALE_DOMAIN}" 2>/dev/null | awk '{print $1}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Fallback: cached IP
|
||||||
|
if [[ -z "$IP" && -f "$CACHE" ]]; then
|
||||||
|
IP=$(grep "^${QUERY} " "$CACHE" 2>/dev/null | awk '{print $2}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$IP" ]]; then
|
||||||
|
echo "Error: cannot resolve ${HOST}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save to cache
|
||||||
|
mkdir -p "$(dirname "$CACHE")"
|
||||||
|
grep -v "^${QUERY} " "$CACHE" > "${CACHE}.tmp" 2>/dev/null || true
|
||||||
|
echo "${QUERY} ${IP}" >> "${CACHE}.tmp"
|
||||||
|
mv "${CACHE}.tmp" "$CACHE"
|
||||||
|
|
||||||
|
exec nc "$IP" "$PORT"
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# === Claude Code for Termux - .bashrc additions ===
|
||||||
|
# Append these lines to your ~/.bashrc
|
||||||
|
|
||||||
|
# Node-gyp / native compilation config
|
||||||
|
export CFLAGS="-I$PREFIX/include"
|
||||||
|
export LDFLAGS="-L$PREFIX/lib"
|
||||||
|
export CC=clang
|
||||||
|
export CXX=clang++
|
||||||
|
export npm_config_nodedir=$PREFIX
|
||||||
|
|
||||||
|
# Add local bin to PATH (for clauded, claude-check-env, etc.)
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
# Prevent Claude Code auto-updater (enforced at shell level too)
|
||||||
|
export DISABLE_AUTOUPDATER=1
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"DISABLE_AUTOUPDATER": "1"
|
||||||
|
},
|
||||||
|
"skipDangerousModePermissionPrompt": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# SSH Config for Termux with mDNS/Tailscale resolution
|
||||||
|
# Copy relevant sections to ~/.ssh/config
|
||||||
|
|
||||||
|
Host *
|
||||||
|
IdentityFile ~/.ssh/id_ed25519
|
||||||
|
IdentitiesOnly yes
|
||||||
|
ServerAliveInterval 60
|
||||||
|
ServerAliveCountMax 3
|
||||||
|
ConnectTimeout 10
|
||||||
|
|
||||||
|
# === mDNS (.local) - Dynamic resolution ===
|
||||||
|
Host *.local
|
||||||
|
User <your-user>
|
||||||
|
ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p
|
||||||
|
|
||||||
|
# === Short hostnames -> mDNS first, then Tailscale ===
|
||||||
|
Host <host-1>
|
||||||
|
User <your-user>
|
||||||
|
ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p
|
||||||
|
|
||||||
|
Host <host-2>
|
||||||
|
User <your-user>
|
||||||
|
ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p
|
||||||
|
|
||||||
|
# === ProxyJump (hosts without Tailscale, via jump host) ===
|
||||||
|
# Host <lan-only-host>
|
||||||
|
# HostName <lan-only-host>.local
|
||||||
|
# User <user>
|
||||||
|
# ProxyJump <jump-host>
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## /tmp Permission Denied
|
||||||
|
|
||||||
|
**Error:** `EACCES: permission denied, mkdir '/tmp/claude/...'`
|
||||||
|
|
||||||
|
**Cause:** Claude Code hardcodes `/tmp` for background tasks. Termux doesn't have `/tmp`.
|
||||||
|
|
||||||
|
**Fix:** Always start Claude via `clauded`, which uses `termux-chroot` to provide a working `/tmp`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Wrong
|
||||||
|
claude
|
||||||
|
|
||||||
|
# Right
|
||||||
|
clauded
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why not proot?** `proot -b $PREFIX/tmp:/tmp` only affects the main process. Child processes (Bash tool, agents) run in separate shells without the binding. `termux-chroot` simulates a full FHS where `/tmp -> /usr/tmp` works for ALL processes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Native Binary Error (v2.1.113+)
|
||||||
|
|
||||||
|
**Error:** `claude native binary not installed`
|
||||||
|
|
||||||
|
**Cause:** Starting with v2.1.113, Anthropic migrated from `cli.js` (pure Node.js) to native binaries (SEA). Termux reports `process.platform === 'android'` which is not supported.
|
||||||
|
|
||||||
|
**Why no workaround:**
|
||||||
|
- `npm install --force` of linux-arm64 downloads the binary but needs `/lib/ld-linux-aarch64.so.1` (glibc) or `/lib/ld-musl-aarch64.so.1` (musl)
|
||||||
|
- Termux uses Bionic libc (Android), neither linker exists
|
||||||
|
- `termux-chroot` and `proot` don't help because the linkers simply don't exist
|
||||||
|
|
||||||
|
**Fix:** Stay on v2.1.112 (last pure Node.js release).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude-update # Automatically respects the version ceiling
|
||||||
|
```
|
||||||
|
|
||||||
|
**How to verify a version is compatible:**
|
||||||
|
```bash
|
||||||
|
npm pack @anthropic-ai/claude-code@VERSION --dry-run 2>&1 | grep cli.js
|
||||||
|
# If cli.js (~13MB) appears -> compatible
|
||||||
|
# If cli-wrapper.cjs (~4KB) appears -> native binary, NOT compatible
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auto-updater Bypasses Settings
|
||||||
|
|
||||||
|
**Problem:** Claude Code may auto-update despite `DISABLE_AUTOUPDATER=1`.
|
||||||
|
|
||||||
|
**Fix:** Protect the installation directory:
|
||||||
|
```bash
|
||||||
|
chmod -R a-w $PREFIX/lib/node_modules/@anthropic-ai/claude-code/
|
||||||
|
```
|
||||||
|
|
||||||
|
To update later, unprotect first:
|
||||||
|
```bash
|
||||||
|
chmod -R u+w $PREFIX/lib/node_modules/@anthropic-ai/claude-code/
|
||||||
|
claude-update
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SSH .local Resolution Fails
|
||||||
|
|
||||||
|
**Error:** `ssh hostname.local` fails with "Could not resolve hostname"
|
||||||
|
|
||||||
|
**Cause:** mDNS (.local) does NOT work natively in Termux (Android's Bionic libc doesn't support it).
|
||||||
|
|
||||||
|
**Fix:** Use the included `ssh-mdns-proxy` as ProxyCommand in `~/.ssh/config`:
|
||||||
|
```
|
||||||
|
Host *.local
|
||||||
|
ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p
|
||||||
|
```
|
||||||
|
|
||||||
|
This uses pure Python multicast UDP to resolve mDNS, with Tailscale DNS fallback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sharp/Image Support
|
||||||
|
|
||||||
|
**Warning:** `Sharp WASM not installed (no image support)`
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
npm install -g @img/sharp-wasm32 sharp --force
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables Claude Code to process images (screenshots, diagrams, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternative: proot-distro
|
||||||
|
|
||||||
|
For running newer Claude Code versions (v2.1.113+), you can use proot-distro with Ubuntu, which provides real glibc:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkg install proot-distro
|
||||||
|
proot-distro install ubuntu
|
||||||
|
proot-distro login ubuntu
|
||||||
|
# Inside Ubuntu: install node, npm, claude-code@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade-offs:** +2GB disk, slight performance overhead, but full Linux compatibility.
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# install.sh: Set up Claude Code on Termux
|
||||||
|
#
|
||||||
|
# Usage: bash install.sh [--dry-run]
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED="\033[0;31m"
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
YELLOW="\033[1;33m"
|
||||||
|
CYAN="\033[0;36m"
|
||||||
|
NC="\033[0m"
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
[[ "$1" == "--dry-run" ]] && DRY_RUN=true
|
||||||
|
|
||||||
|
CLAUDE_VERSION="2.1.112"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BIN_DIR="$HOME/.local/bin"
|
||||||
|
|
||||||
|
step() { echo -e "\n${CYAN}[$1/$TOTAL] $2${NC}"; }
|
||||||
|
run() {
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${YELLOW}[dry-run]${NC} $*"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify we're on Termux
|
||||||
|
if [[ -z "$PREFIX" || ! -d "$PREFIX" ]]; then
|
||||||
|
echo -e "${RED}Error: Not running on Termux${NC}"
|
||||||
|
echo "This installer is designed for Termux on Android."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOTAL=8
|
||||||
|
echo -e "${GREEN}=== Claude Code for Termux - Installer ===${NC}"
|
||||||
|
echo "Version ceiling: v$CLAUDE_VERSION"
|
||||||
|
$DRY_RUN && echo -e "${YELLOW}(dry-run mode - no changes will be made)${NC}"
|
||||||
|
|
||||||
|
# 1. Install system packages
|
||||||
|
step 1 "Installing system packages..."
|
||||||
|
run pkg install -y nodejs python git proot
|
||||||
|
|
||||||
|
# 2. Install Claude Code
|
||||||
|
step 2 "Installing Claude Code v$CLAUDE_VERSION..."
|
||||||
|
run npm install -g "@anthropic-ai/claude-code@$CLAUDE_VERSION"
|
||||||
|
|
||||||
|
# 3. Protect against auto-updater
|
||||||
|
step 3 "Protecting against auto-updater..."
|
||||||
|
CLAUDE_DIR="$PREFIX/lib/node_modules/@anthropic-ai/claude-code"
|
||||||
|
if [[ -d "$CLAUDE_DIR" ]]; then
|
||||||
|
run chmod -R a-w "$CLAUDE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Install scripts
|
||||||
|
step 4 "Installing scripts to $BIN_DIR..."
|
||||||
|
run mkdir -p "$BIN_DIR"
|
||||||
|
for script in clauded claude-check-env claude-update mdns-resolve ssh-mdns-proxy; do
|
||||||
|
run cp "$SCRIPT_DIR/bin/$script" "$BIN_DIR/$script"
|
||||||
|
run chmod +x "$BIN_DIR/$script"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 5. Claude settings
|
||||||
|
step 5 "Configuring Claude settings..."
|
||||||
|
run mkdir -p "$HOME/.claude"
|
||||||
|
if [[ ! -f "$HOME/.claude/settings.json" ]]; then
|
||||||
|
run cp "$SCRIPT_DIR/config/settings.json" "$HOME/.claude/settings.json"
|
||||||
|
else
|
||||||
|
echo " ~/.claude/settings.json already exists, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Bashrc additions
|
||||||
|
step 6 "Updating ~/.bashrc..."
|
||||||
|
if ! grep -q "DISABLE_AUTOUPDATER" "$HOME/.bashrc" 2>/dev/null; then
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
echo "" >> "$HOME/.bashrc"
|
||||||
|
cat "$SCRIPT_DIR/config/bashrc.snippet" >> "$HOME/.bashrc"
|
||||||
|
fi
|
||||||
|
echo " Added Claude-related exports to ~/.bashrc"
|
||||||
|
else
|
||||||
|
echo " ~/.bashrc already configured, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7. Install sharp-wasm32 (image support)
|
||||||
|
step 7 "Installing Sharp WASM (image support)..."
|
||||||
|
run npm install -g @img/sharp-wasm32 sharp --force 2>/dev/null || {
|
||||||
|
echo -e " ${YELLOW}Sharp install failed (optional, images won't work)${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 8. Done
|
||||||
|
step 8 "Verifying installation..."
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
echo ""
|
||||||
|
"$BIN_DIR/claude-check-env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}=== Installation Complete ===${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Restart your shell: source ~/.bashrc"
|
||||||
|
echo " 2. Authenticate: claude auth"
|
||||||
|
echo " 3. Start Claude: clauded"
|
||||||
|
echo ""
|
||||||
|
echo "See docs/TROUBLESHOOTING.md for common issues."
|
||||||
Loading…
Reference in New Issue