diff --git a/README.md b/README.md index 8051250..abbc356 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Claude Code v2.1.113+ switched to native binaries, breaking Termux/Android compa ## Quick Install ```bash -git clone /andres/claude-code-termux.git +git clone //claude-code-termux.git cd claude-code-termux bash install.sh ``` @@ -40,7 +40,9 @@ bash install.sh --dry-run | `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 | +| `mdns-publish.py` | `~/.local/bin/` | Publish this device as `movil.local` (needs `zeroconf`) | +| `ssh-mdns-proxy` | `~/.local/bin/` | SSH ProxyCommand: mDNS → Tailscale | +| `ssh-fallback` | `~/.local/bin/` | SSH ProxyCommand: Tailscale → LAN fallback | | `settings.json` | `~/.claude/` | Disables auto-updater | ## Usage @@ -71,6 +73,19 @@ export TAILSCALE_DOMAIN="your-tailnet.ts.net" `claude-update` enforces this automatically. See `docs/TROUBLESHOOTING.md` for details. +## Docs + +Step-by-step guides for each piece of the stack: + +| # | Doc | Covers | +|---|-----|--------| +| 01 | [docs/01-termux-fresh-install.md](docs/01-termux-fresh-install.md) | Clean Termux install (F-Droid / GitHub Releases, not Play) | +| 02 | [docs/02-base-packages.md](docs/02-base-packages.md) | Required `pkg` / `pip` / `npm` packages | +| 03 | [docs/03-ssh-mdns-tailscale.md](docs/03-ssh-mdns-tailscale.md) | SSH, `.local` resolution, Tailscale | +| 04 | [docs/04-samba.md](docs/04-samba.md) | Samba server on port 4450 + `msg.lock` gotcha | +| 05 | [docs/05-claude-code.md](docs/05-claude-code.md) | Claude Code internals (version ceiling, `clauded`, settings) | +| — | [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common errors and fixes | + ## Troubleshooting See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for: diff --git a/bin/mdns-publish.py b/bin/mdns-publish.py new file mode 100755 index 0000000..189c946 --- /dev/null +++ b/bin/mdns-publish.py @@ -0,0 +1,73 @@ +#!/data/data/com.termux/files/usr/bin/python +"""Publica servicios via mDNS/Zeroconf: SSH + Samba""" +import socket +import signal +import sys +from zeroconf import ServiceInfo, Zeroconf, IPVersion + +HOSTNAME = 'movil' + +SERVICES = [ + { + 'type': '_ssh._tcp.local.', + 'port': 8022, + 'props': {'description': 'Termux SSH'}, + }, + { + 'type': '_smb._tcp.local.', + 'port': 4450, + 'props': {'description': 'Termux Samba', 'path': '/'}, + }, +] + + +def get_local_ip(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + return '127.0.0.1' + + +def main(): + ip = get_local_ip() + addr = socket.inet_aton(ip) + zc = Zeroconf(ip_version=IPVersion.V4Only) + infos = [] + + for svc in SERVICES: + info = ServiceInfo( + svc['type'], + f'{HOSTNAME}.{svc["type"]}', + addresses=[addr], + port=svc['port'], + properties=svc['props'], + server=f'{HOSTNAME}.local.', + ) + zc.register_service(info) + infos.append(info) + print(f'Publicando {HOSTNAME}.local {svc["type"]} -> {ip}:{svc["port"]}') + + def cleanup(sig, frame): + print('Deteniendo mDNS...') + for info in infos: + zc.unregister_service(info) + zc.close() + sys.exit(0) + + signal.signal(signal.SIGINT, cleanup) + signal.signal(signal.SIGTERM, cleanup) + + try: + signal.pause() + except AttributeError: + import time + while True: + time.sleep(3600) + + +if __name__ == '__main__': + main() diff --git a/bin/ssh-fallback b/bin/ssh-fallback new file mode 100755 index 0000000..bcd6ac1 --- /dev/null +++ b/bin/ssh-fallback @@ -0,0 +1,23 @@ +#!/data/data/com.termux/files/usr/bin/bash +# +# ssh-fallback: Proxy para SSH con fallback automático +# Usado como ProxyCommand en ~/.ssh/config +# +# Uso: ssh-fallback + +TAILSCALE_HOST="$1" +LAN_IP="$2" +PORT="${3:-22}" + +# Intentar Tailscale primero (1 segundo timeout) +if ping -c 1 -W 1 "$TAILSCALE_HOST" &>/dev/null 2>&1; then + exec nc -w 5 "$TAILSCALE_HOST" "$PORT" +fi + +# Fallback a IP LAN +if ping -c 1 -W 1 "$LAN_IP" &>/dev/null 2>&1; then + exec nc -w 5 "$LAN_IP" "$PORT" +fi + +# Último intento: Tailscale sin verificar +exec nc -w 10 "$TAILSCALE_HOST" "$PORT" diff --git a/config/bashrc.snippet b/config/bashrc.snippet index c7b59d9..52e29b2 100644 --- a/config/bashrc.snippet +++ b/config/bashrc.snippet @@ -13,3 +13,24 @@ export PATH="$HOME/.local/bin:$PATH" # Prevent Claude Code auto-updater (enforced at shell level too) export DISABLE_AUTOUPDATER=1 + +# === Optional: helper aliases for samba + mDNS services === +# Only useful if you set up smbd and mdns-publish (see docs/04-samba.md, docs/03-ssh-mdns-tailscale.md). + +alias smb-start='smbd -D -s ~/.config/samba/smb.conf 2>>~/.config/samba/samba-startup.log && echo "Samba started on port 4450"' +alias smb-stop='pkill smbd && echo "Samba stopped"' +alias smb-restart='pkill smbd 2>/dev/null; sleep 1; smbd -D -s ~/.config/samba/smb.conf 2>>~/.config/samba/samba-startup.log && echo "Samba restarted"' +alias smb-status='pgrep -a smbd && echo "Port: 4450" || echo "Samba is not running"' +alias smb-log='tail -50 ~/.config/samba/samba.log' + +alias mdns-status='pgrep -af mdns-publish || echo "mDNS publisher is not running"' +alias mdns-restart='pkill -f mdns-publish; sleep 1; nohup python ~/.local/bin/mdns-publish.py >>~/.cache/mdns.log 2>&1 & disown && echo "mDNS restarted"' +alias mdns-stop='pkill -f mdns-publish && echo "mDNS stopped"' + +# === Optional: auto-start samba + mDNS on shell login === +# Uncomment the block below if you want services up automatically. +# Failures are logged (NOT silenced) — silent failures after a tar restore are the worst class of bug. +# +# mkdir -p ~/.cache +# pgrep -x smbd >/dev/null 2>&1 || smbd -D -s ~/.config/samba/smb.conf 2>>~/.config/samba/samba-startup.log +# pgrep -f "mdns-publish" >/dev/null 2>&1 || nohup python ~/.local/bin/mdns-publish.py >>~/.cache/mdns.log 2>&1 & disown diff --git a/config/smb.conf.example b/config/smb.conf.example new file mode 100644 index 0000000..bd273e8 --- /dev/null +++ b/config/smb.conf.example @@ -0,0 +1,69 @@ +# Samba on Termux — port 4450 (445 requires root) +# +# Replace YOUR_USER with the Samba user you created via `smbpasswd -a YOUR_USER`. +# Replace HOST_NAME with whatever you want this device to advertise as. +# Connect from clients with: \\:4450 or smb://:4450/ + +[global] + workgroup = WORKGROUP + server string = Termux Mobile + netbios name = HOST_NAME + + # Non-privileged port + smb ports = 4450 + + security = user + map to guest = never + passdb backend = tdbsam + + # SMB2/3 only — Windows 11 and modern Linux/macOS prefer these + client min protocol = SMB2 + server min protocol = SMB2 + server max protocol = SMB3 + client max protocol = SMB3 + + smb encrypt = desired + server signing = desired + client signing = desired + + # Termux paths (these MUST exist before smbd starts) + private dir = /data/data/com.termux/files/home/.config/samba/private + lock directory = /data/data/com.termux/files/home/.config/samba/lock + state directory = /data/data/com.termux/files/home/.config/samba/state + cache directory = /data/data/com.termux/files/home/.config/samba/cache + pid directory = /data/data/com.termux/files/home/.config/samba + + log file = /data/data/com.termux/files/home/.config/samba/samba.log + max log size = 500 + log level = 1 + + socket options = TCP_NODELAY IPTOS_LOWDELAY + use sendfile = yes + aio read size = 16384 + aio write size = 16384 + + unix charset = UTF-8 + dos charset = CP850 + + server multi channel support = yes + mdns name = mdns + +# Share: Android shared storage (DCIM, Downloads, etc.) +[Storage] + comment = Android shared storage + path = /storage/emulated/0 + browseable = yes + writable = yes + valid users = YOUR_USER + create mask = 0644 + directory mask = 0755 + +# Share: Termux $HOME (be careful, this exposes shell config too) +[Termux] + comment = Termux home + path = /data/data/com.termux/files/home + browseable = yes + writable = yes + valid users = YOUR_USER + create mask = 0644 + directory mask = 0755 diff --git a/docs/01-termux-fresh-install.md b/docs/01-termux-fresh-install.md new file mode 100644 index 0000000..4987990 --- /dev/null +++ b/docs/01-termux-fresh-install.md @@ -0,0 +1,59 @@ +# 01 — Fresh Termux install (Android) + +This guide covers a clean Termux install **without** Google Play, which is required for Termux:API / Termux:Boot compatibility. + +## Why not Google Play + +Google Play freezes Termux on an old version. Addons (Termux:API, Termux:Boot, Termux:Widget) require signature parity with the base app, so the Play version is incompatible with the F-Droid / GitHub-Release addons. + +**Decision:** install everything from F-Droid or GitHub Releases (Termux team's official builds). + +## Recommended source + +Use one of: +- [F-Droid](https://f-droid.org/en/packages/com.termux/) — auto-updates, signed by F-Droid +- [GitHub Releases](https://github.com/termux/termux-app/releases) — latest, signed by the Termux team + +Both share the same signing key, so addons work across them. **Don't mix Play and F-Droid/GitHub.** + +## APK list (download to the device beforehand) + +| Package | Purpose | Required | +|---------|---------|----------| +| `termux-app_*.apk` | Base shell | yes | +| `termux-api-app_*.apk` | Sensors, camera, clipboard | recommended | +| `termux-boot-app_*.apk` | Auto-start on boot | optional | +| `termux-widget-app_*.apk` | Home-screen launchers | optional | + +## First boot + +```bash +# 1. Allow Termux access to shared storage (~/storage symlink) +termux-setup-storage + +# 2. Prevent Android from killing the process when screen is off +termux-wake-lock + +# 3. Update and install the minimum +pkg update -y +pkg install -y openssh nodejs git proot python +``` + +## SSH for remote access (optional but useful) + +```bash +passwd # set an SSH password +sshd # start the daemon on port 8022 +whoami # note the u0_a*** username for remote login +``` + +From another machine: +```bash +ssh -p 8022 u0_a***@ +``` + +If the device is on Tailscale, use the Tailscale IP instead of the LAN one. Termux itself can't run the Tailscale CLI; use the Android Tailscale app and the device will appear in your tailnet automatically. + +## What's next + +Continue with `02-base-packages.md` to install the rest of the stack used by Claude Code and helper scripts. diff --git a/docs/02-base-packages.md b/docs/02-base-packages.md new file mode 100644 index 0000000..0c94dbd --- /dev/null +++ b/docs/02-base-packages.md @@ -0,0 +1,66 @@ +# 02 — Base packages + +Minimum set of packages needed by Claude Code and the helper scripts in this repo. Everything here is idempotent — re-running is safe. + +## Required (Claude Code itself) + +```bash +pkg install -y \ + nodejs \ + python \ + git \ + proot \ + ripgrep +``` + +| Package | Why | +|---------|-----| +| `nodejs` | Claude Code runs on Node.js | +| `python` | `mdns-resolve`, `mdns-publish.py` | +| `git` | Cloning this repo, version control inside Claude | +| `proot` | Provides `termux-chroot` used by `clauded` (fixes `/tmp`) | +| `ripgrep` | Faster search inside Claude (the bundled one may not run on aarch64) | + +## Recommended (SSH + mDNS) + +```bash +pkg install -y openssh termux-api +pip install zeroconf +``` + +| Package | Why | +|---------|-----| +| `openssh` | SSH server (`sshd`) and client | +| `termux-api` | Backs the `termux-*` Android-bridge commands | +| `zeroconf` (pip) | Required by `mdns-publish.py`; **does not survive a `tar` backup** of `$HOME`, reinstall after restore | + +## Optional (file sharing) + +```bash +pkg install -y samba +``` + +See `04-samba.md` for the rest of the setup (custom port 4450, share definitions). + +## Optional (image support in Claude) + +```bash +npm install -g @img/sharp-wasm32 sharp --force +``` + +Enables image input (screenshots, diagrams). Sometimes flaky on aarch64; safe to skip. + +## Verifying + +```bash +node --version # >= 22 LTS recommended +python --version # 3.11+ +proot --version +ssh -V +``` + +If any of these fail, re-run the relevant `pkg install`. + +## What's next + +`03-ssh-mdns-tailscale.md` — set up dynamic hostname resolution so you never hardcode IPs. diff --git a/docs/03-ssh-mdns-tailscale.md b/docs/03-ssh-mdns-tailscale.md new file mode 100644 index 0000000..778018c --- /dev/null +++ b/docs/03-ssh-mdns-tailscale.md @@ -0,0 +1,60 @@ +# 03 — SSH, mDNS and Tailscale on Termux + +Termux's Bionic libc does **not** resolve `.local` names. This repo ships small Python helpers that fix this without needing root or a custom resolver. + +## Scripts involved + +| Script | Role | +|--------|------| +| `mdns-resolve` | Resolve `.local` via raw multicast UDP. Returns the IP or non-zero. | +| `mdns-publish.py` | Publish this device on the LAN as `movil.local` (or whatever you choose) using `zeroconf`. | +| `ssh-mdns-proxy` | SSH `ProxyCommand` that tries mDNS first, then Tailscale DNS. Used from `~/.ssh/config`. | +| `ssh-fallback` | Lower-level fallback: tries Tailscale, then LAN. Useful when Tailscale is flaky. | + +## SSH config + +Drop the contents of `config/ssh_config.example` into `~/.ssh/config` (or merge it). + +The key entries: + +```ssh-config +Host *.local + ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p + +Host lenovo-ideapad + ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p + +Host *.YOUR-TAILNET.ts.net + User YOUR_USER +``` + +Adjust the username and the tailnet domain. The proxy command will try mDNS first, then fall through to your Tailscale magic-DNS name (env `TAILSCALE_DOMAIN`). + +## Publishing this device as `movil.local` + +If you want the device to be reachable as `movil.local` from your LAN: + +```bash +# One-shot foreground (to see errors): +python ~/.local/bin/mdns-publish.py + +# Background (typical use, from ~/.bashrc): +nohup python ~/.local/bin/mdns-publish.py >>~/.cache/mdns.log 2>&1 & +disown +``` + +**Gotcha:** `zeroconf` (the pip package) is **not** part of a `pkg`-managed install. If you restore Termux from a `tar` backup, `zeroconf` will be missing from `$PREFIX/lib/python*/site-packages` and the script will silently fail with `ModuleNotFoundError`. Re-run `pip install zeroconf` after every restore. + +## Logging, not silencing + +The `.bashrc` auto-start lines in this repo (`config/bashrc.snippet`) deliberately log to a file instead of `/dev/null`. Silent failures are the single most painful class of bug after a restore — keep the logs. + +## Tailscale + +Tailscale on Termux runs **as the Android app**, not as a CLI inside Termux. You can't `tailscale up` from a shell. The app provides magic DNS for `*.YOUR-TAILNET.ts.net`, which Bionic-libc resolves fine (it's just a regular DNS query). + +When you list hosts in `~/.ssh/config`, prefer: +1. `.local` for fast LAN access +2. `.YOUR-TAILNET.ts.net` for off-LAN + +Never hardcode IPs — they rotate. diff --git a/docs/04-samba.md b/docs/04-samba.md new file mode 100644 index 0000000..4161e5f --- /dev/null +++ b/docs/04-samba.md @@ -0,0 +1,75 @@ +# 04 — Samba server on Termux (port 4450) + +Optional but useful: expose a few folders over SMB so other devices on the LAN can read/write files without ADB. + +## Why port 4450 + +Ports below 1024 require root. Samba on Termux runs unprivileged, so we use a high port (`4450`) and configure clients to point at it explicitly. + +``` +\\:4450 +``` + +Most file managers accept `smb://:4450/`. + +## Install + +```bash +pkg install -y samba +mkdir -p ~/.config/samba/lock ~/.config/samba/private +``` + +## Configuration + +Copy `config/smb.conf.example` to `~/.config/samba/smb.conf` and edit the share paths to match your device. Then add a Samba user: + +```bash +smbpasswd -a YOUR_USER +``` + +This creates `~/.config/samba/private/passdb.tdb`. + +## Start / stop + +```bash +smbd -D -s ~/.config/samba/smb.conf 2>>~/.config/samba/samba-startup.log +pkill smbd +``` + +The aliases in `config/bashrc.snippet` (`smb-start`, `smb-stop`, `smb-restart`, `smb-status`, `smb-log`) wrap these. + +## Critical gotcha — `msg.lock` permissions after restore + +If you restore `~/.config/samba/` from a `tar` backup, the `lock/msg.lock` directory ends up with mode `0700` because `tar` preserves the source attributes. `smbd` requires `0755` on this directory and refuses to start with: + +``` +invalid permissions on directory '.../msg.lock': has 0700 should be 0755 +``` + +**Fix:** + +```bash +chmod 0755 ~/.config/samba/lock/msg.lock +``` + +This must be re-run after every restore from a backup that includes the samba lock dir. + +## Connecting from another machine + +```bash +# Linux +smbclient -L // -p 4450 -U YOUR_USER +mount -t cifs ///Home /mnt -o port=4450,username=YOUR_USER + +# Windows: \\:4450 in Explorer + +# macOS: Finder → Go → Connect to Server → smb://:4450 +``` + +## Discovery via mDNS + +If you also run `mdns-publish.py` (see `03-ssh-mdns-tailscale.md`), the device shows up as `movil.local` and SMB clients can use that name directly. + +## Don't bind to `0.0.0.0` if you don't need to + +The default `smb.conf` example binds to all interfaces. If the device roams across hostile WiFi networks, restrict `interfaces =` and `bind interfaces only = yes` to the LAN interface (`wlan0` on most devices). diff --git a/docs/05-claude-code.md b/docs/05-claude-code.md new file mode 100644 index 0000000..9d71e22 --- /dev/null +++ b/docs/05-claude-code.md @@ -0,0 +1,84 @@ +# 05 — Claude Code on Termux + +The reason this whole repo exists. Two non-obvious things break Claude Code on Termux: + +1. **Version ceiling: v2.1.112.** Starting v2.1.113, the CLI ships as a native binary (SEA) for `linux-x64`, `linux-arm64-glibc`, `linux-arm64-musl`, `darwin-*`, `win32-*`. Termux's `process.platform === 'android'` is not in that list. Even `--force`-installing the `linux-arm64` package fails because the binary needs `/lib/ld-linux-aarch64.so.1` or `/lib/ld-musl-aarch64.so.1`, neither of which exist in Bionic libc. +2. **`/tmp` is hardcoded.** Claude Code writes to `/tmp/claude/...` for background tasks. Termux doesn't have a writable `/tmp`. `termux-chroot` exposes `/tmp -> /usr/tmp` to **all child processes** (which `proot -b` alone does not). + +## Install + +If you just want it working, run the top-level installer: + +```bash +bash install.sh +``` + +Otherwise, the manual steps are: + +```bash +# 1. Pin to the last compatible version +npm install -g @anthropic-ai/claude-code@2.1.112 + +# 2. Protect against the auto-updater (it can ignore env vars) +chmod -R a-w $PREFIX/lib/node_modules/@anthropic-ai/claude-code/ + +# 3. Make the wrapper available +cp bin/clauded ~/.local/bin/ +chmod +x ~/.local/bin/clauded +``` + +## Settings + +`~/.claude/settings.json`: + +```json +{ + "env": { "DISABLE_AUTOUPDATER": "1" }, + "skipDangerousModePermissionPrompt": true, + "effortLevel": "xhigh" +} +``` + +`DISABLE_AUTOUPDATER` is also exported from `bashrc.snippet`. Belt and suspenders — the auto-updater has been observed to ignore the env var in some releases, which is why `chmod -R a-w` is the real defense. + +## How `clauded` works + +``` +clauded + └── termux-chroot + └── claude --dangerously-skip-permissions "$@" +``` + +`termux-chroot` simulates a Linux FHS where `/tmp`, `/etc`, `/usr` etc. point to their Termux equivalents. Crucially, every child process inherits this view, so any background task Claude spawns also sees a working `/tmp`. + +## Updating safely + +```bash +claude-update +``` + +This script: +- Checks npm for the latest version. +- Compares against the ceiling (`2.1.112`). +- Refuses to upgrade past the ceiling. +- Reapplies `chmod -R a-w` after a successful install. + +If a future Anthropic release re-supports Android (or restores the pure-Node fallback), bump the ceiling in `claude-update` and the README. + +## Authenticating + +```bash +clauded +# Inside Claude: /login +``` + +Subscription accounts and API keys both work. The OAuth flow opens a URL; long-press to copy it on Android and paste it in a real browser (Chromium, Firefox). + +## When the wheels come off + +See `TROUBLESHOOTING.md` for: +- `EACCES: /tmp/claude/...` +- `native binary not installed` +- `claude --version` works but `clauded` errors +- Sharp / image support +- `proot-distro` escape hatch (lets you run newer versions inside a real glibc rootfs) diff --git a/install.sh b/install.sh index 04acb1b..f386751 100755 --- a/install.sh +++ b/install.sh @@ -36,7 +36,7 @@ if [[ -z "$PREFIX" || ! -d "$PREFIX" ]]; then exit 1 fi -TOTAL=8 +TOTAL=9 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}" @@ -59,7 +59,7 @@ 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 +for script in clauded claude-check-env claude-update mdns-resolve mdns-publish.py ssh-mdns-proxy ssh-fallback; do run cp "$SCRIPT_DIR/bin/$script" "$BIN_DIR/$script" run chmod +x "$BIN_DIR/$script" done @@ -91,8 +91,14 @@ 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..." +# 8. Install zeroconf for mDNS publisher (needed by mdns-publish.py) +step 8 "Installing zeroconf (pip) for mDNS publishing..." +run pip install --quiet zeroconf 2>/dev/null || { + echo -e " ${YELLOW}zeroconf install failed (optional, mdns-publish won't work)${NC}" +} + +# 9. Done +step 9 "Verifying installation..." if ! $DRY_RUN; then echo "" "$BIN_DIR/claude-check-env"