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:
Andrés Eduardo García Márquez 2026-04-26 17:30:25 -05:00
commit 0d5d099dee
12 changed files with 690 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.bak
*.tmp
.DS_Store

84
README.md Normal file
View File

@ -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

76
bin/claude-check-env Executable file
View File

@ -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)"

68
bin/claude-update Executable file
View File

@ -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)"

43
bin/clauded Executable file
View File

@ -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 "$@"

103
bin/mdns-resolve Executable file
View File

@ -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()

48
bin/ssh-mdns-proxy Executable file
View File

@ -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"

15
config/bashrc.snippet Normal file
View File

@ -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

6
config/settings.json Normal file
View File

@ -0,0 +1,6 @@
{
"env": {
"DISABLE_AUTOUPDATER": "1"
},
"skipDangerousModePermissionPrompt": true
}

29
config/ssh_config.example Normal file
View File

@ -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>

106
docs/TROUBLESHOOTING.md Normal file
View File

@ -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.

109
install.sh Executable file
View File

@ -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."