Repository navigation
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·319 lines (264 loc) · 12.7 KB
/
Copy pathsetup.sh
File metadata and controls
executable file
·319 lines (264 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
#!/data/data/com.termux/files/usr/bin/bash
#
# Claude Code on Android — automated installer (proot-Ubuntu / Path B)
# Companion to the DevZoneX video. Run inside Termux on an unrooted Android phone.
#
# WHAT THIS DOES (README Chapters 1-3):
# 1. Termux packages: proot-distro, nodejs
# 2. Ubuntu container via proot-distro
# 3. Claude Code inside Ubuntu, via Anthropic's official installer
#
# WHAT THIS DOES NOT DO:
# 9Router setup, provider API keys, combos, and ~/.claude/settings.json.
# Those need your own keys and a browser, so they stay manual.
# The script prints the next steps when it finishes.
#
# Safe to re-run. Already-completed steps are detected and skipped.
#
# Version: 1.2 (2026-07-27)
# Tested: 2026-07-26, clean run on a freshly wiped Termux (aarch64, unrooted)
# 1.2: retry pkg, curl, and the Ubuntu download; pre-flight connectivity check;
# fully non-interactive pkg/apt so nothing can hang on a prompt
# 1.1: auto-retry apt once after clearing the index (fixes 'Hash Sum mismatch')
# Usage:
# curl -fsSL https://raw.githubusercontent.com/iAmAjayTeli/claude-code-android/main/setup.sh -o setup.sh
# cat setup.sh # read it first
# bash setup.sh
set -uo pipefail # deliberately NOT -e: pkg upgrade can exit non-zero on
# harmless prompts. Every step below is checked explicitly.
VERSION="1.2"
MIN_FREE_MB=5000
export MAX_TRIES=3 # how many times to attempt a network operation before giving up
# exported so the apt_retry helper inside Ubuntu can see it
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$1"; }
ok() { printf '\033[1;32m ✓ %s\033[0m\n' "$1"; }
warn() { printf '\n\033[1;33m!! %s\033[0m\n' "$1"; }
die() { printf '\n\033[1;31mXX %s\033[0m\n\n' "$1" >&2; exit 1; }
# Retry a command up to MAX_TRIES times, with a short pause between attempts.
# Used for every network operation so a single dropped packet can't fail the
# whole install. "$@" is the command; the label is printed on each retry.
retry() {
local label="$1"; shift
local n=1
while ! "$@"; do
n=$((n + 1))
if [ "$n" -gt "$MAX_TRIES" ]; then
echo " !! $label failed after $MAX_TRIES attempts"
return 1
fi
echo " -- $label failed, retrying (attempt $n/$MAX_TRIES)..."
sleep 3
done
}
# --- what runs INSIDE ubuntu ---------------------------------------------------
# Quoted heredoc: nothing expands here in Termux. $HOME resolves inside Ubuntu.
read -r -d '' UBUNTU_SETUP <<'EOS' || true
set -e
export DEBIAN_FRONTEND=noninteractive
# --force-confold keeps existing config files instead of stopping to ask.
# Without it an unattended apt upgrade can hang forever on a dpkg prompt.
APT_OPTS='-y -qq -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef'
# Run an apt command; on failure, assume a stale index or a caching proxy
# served a mismatched file ("Hash Sum mismatch"), wipe the lists, re-update
# with caching disabled, and retry up to MAX_TRIES times. This is the single
# most common transient failure on mobile connections.
apt_retry() {
local label="$1"; shift
local max="${MAX_TRIES:-3}"
local n=1
# shellcheck disable=SC2086
while ! apt-get $APT_OPTS "$@"; do
n=$((n + 1))
if [ "$n" -gt "$max" ]; then
echo " !! $label failed after $max attempts"
return 1
fi
echo "--- $label failed; clearing the package index and retrying (attempt $n/$max)"
apt-get clean
rm -rf /var/lib/apt/lists/*
apt-get update -qq -o Acquire::http::No-Cache=true
# shellcheck disable=SC2086
apt-get $APT_OPTS -o Acquire::http::No-Cache=true "$@"
done
}
echo "--- apt update / upgrade"
apt-get update -qq || apt-get update -qq -o Acquire::http::No-Cache=true
apt_retry "apt upgrade" upgrade
echo "--- installing curl git wget build-essential"
apt_retry "apt install" install curl git wget build-essential
if [ -x "$HOME/.local/bin/claude" ]; then
echo "--- claude already present, skipping installer"
else
echo "--- running Anthropic's official installer"
# Download to a file first rather than piping straight into bash: with a
# bare pipe, a failed download feeds bash an empty script that exits 0,
# so the failure is silent and only shows up later as "command not found".
# --retry/--retry-all-errors re-fetches on any transient failure.
curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors \
https://claude.ai/install.sh -o /tmp/claude-install.sh
[ -s /tmp/claude-install.sh ] || { echo "!! installer download was empty"; exit 1; }
bash /tmp/claude-install.sh
rm -f /tmp/claude-install.sh
fi
# idempotent PATH line
if ! grep -q '.local/bin' "$HOME/.bashrc" 2>/dev/null; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
echo "--- added ~/.local/bin to PATH in .bashrc"
fi
export PATH="$HOME/.local/bin:$PATH"
echo "--- verifying"
claude --version
EOS
# --- sanity checks -------------------------------------------------------------
printf '\n\033[1mClaude Code on Android — automated setup v%s\033[0m\n' "$VERSION"
printf 'Termux -> Ubuntu -> Claude Code. 9Router stays manual.\n'
[ -d /data/data/com.termux ] || die "This must run inside Termux.
Install Termux from F-Droid or GitHub Releases — NOT the Play Store."
arch=$(uname -m)
case "$arch" in
aarch64|arm64) ok "architecture $arch" ;;
*) die "Unsupported architecture: $arch
Claude Code needs aarch64. Some budget phones ship a 32-bit OS on 64-bit
hardware; there is no workaround for those." ;;
esac
avail_mb=$(df -m "$HOME" 2>/dev/null | awk 'NR==2 {print $4}')
if [ -n "${avail_mb:-}" ] && [ "$avail_mb" -lt "$MIN_FREE_MB" ]; then
warn "Only ${avail_mb}MB free. Ubuntu is ~2GB and unpacks before cleanup;
${MIN_FREE_MB}MB recommended."
printf '\nContinue anyway? [y/N] '
read -r reply
case "$reply" in [yY]*) ;; *) die "Stopped. Free up space and re-run." ;; esac
else
ok "${avail_mb:-?}MB free"
fi
if command -v claude >/dev/null 2>&1; then
warn "A 'claude' command already exists in TERMUX: $(command -v claude)
That is almost certainly a leftover native (Path A) install. It will not
interfere with the Ubuntu one, but having two different 'claude' commands
in two shells gets confusing. Consider removing it afterwards."
fi
# --- connectivity pre-flight --------------------------------------------------
# The single most confusing on-camera failure is downloading for minutes
# only to discover the network is down. Check first, and bail with a clear
# message if there's no connection. --max-time keeps it from hanging.
say "Checking network"
if curl -fsS --max-time 10 -o /dev/null https://claude.ai/install.sh 2>/dev/null \
|| curl -fsS --max-time 10 -o /dev/null https://github.com 2>/dev/null; then
ok "internet reachable"
else
die "No internet connection, or claude.ai is unreachable.
The install downloads packages and a ~2GB Ubuntu image, so it needs a
stable connection. Check Wi-Fi/mobile data and re-run. Completed steps
are skipped, so there's no cost to re-running."
fi
# --- step 1: termux packages ---------------------------------------------------
say "Step 1/3 — Termux packages"
printf 'If asked about package maintainer configs, press y.\n\n'
# -o Dpkg::Options::=--force-confold makes pkg fully non-interactive: it
# keeps existing config files instead of stopping on a conffile prompt that
# would hang an unattended run.
retry "pkg update" pkg update -y -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef
retry "pkg upgrade" pkg upgrade -y -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef
retry "pkg install" pkg install proot-distro nodejs -y
command -v proot-distro >/dev/null || die "proot-distro did not install.
Try 'pkg install proot-distro -y' manually and read the output."
ok "proot-distro ready"
if command -v node >/dev/null 2>&1; then
ok "node $(node -v) — needed later for 9Router"
else
warn "Node.js did not install. Claude Code does not need it, but 9Router
does. Fix with 'pkg install nodejs -y' before the 9Router step."
fi
# --- step 2: ubuntu ------------------------------------------------------------
# Check the rootfs directory directly rather than parsing `proot-distro list`.
# The --installed flag is not available on every proot-distro version, and if
# the check silently fails we would try to reinstall over a working container.
# Verify /bin/bash exists INSIDE the rootfs, not just the directory: a dropped
# download can leave a half-written rootfs dir that's not a real install, and
# skipping that as "installed" would produce a broken container silently.
UBUNTU_ROOTFS="${PREFIX:-/data/data/com.termux/files/usr}/var/lib/proot-distro/installed-rootfs/ubuntu"
ubuntu_installed() { [ -x "$UBUNTU_ROOTFS/bin/bash" ]; }
if ubuntu_installed; then
say "Step 2/3 — Ubuntu already installed, skipping"
else
# If a partial rootfs dir exists from a dropped previous run, remove it so
# the fresh install doesn't collide with stale, half-extracted files.
if [ -d "$UBUNTU_ROOTFS" ] && ! ubuntu_installed; then
warn "Found a partial Ubuntu install (no /bin/bash inside). Removing it and starting fresh."
proot-distro remove ubuntu >/dev/null 2>&1 || rm -rf "$UBUNTU_ROOTFS"
fi
say "Step 2/3 — installing Ubuntu (~2GB, usually 2-5 minutes)"
# Retry the download: it's a large pull and mobile connections drop mid-way.
if ! retry "proot-distro install" proot-distro install ubuntu; then
if ubuntu_installed; then
warn "Install reported an error but the container is complete. Continuing."
else
die "Ubuntu install failed after $MAX_TRIES attempts and no container was created.
Usually a dropped connection mid-download. Re-run this script — the
partial download is discarded and it starts fresh.
Only if a later run keeps failing on a half-written container should you
reset it — and be aware this DELETES everything inside Ubuntu:
proot-distro remove ubuntu && proot-distro install ubuntu"
fi
fi
fi
ok "Ubuntu container ready"
# --- step 3: claude code inside ubuntu -----------------------------------------
say "Step 3/3 — installing Claude Code inside Ubuntu"
printf 'This runs apt and then Anthropic'\''s official installer. Output follows.\n'
if proot-distro login ubuntu -- bash -c "$UBUNTU_SETUP"; then
ok "Claude Code installed and responding inside Ubuntu"
else
die "The Ubuntu step failed. The script already retried once after clearing
the package index, so this is probably not a stale mirror.
If the error was 'Hash Sum mismatch', it is a transient mirror/proxy issue,
not your setup. Switch network (Wi-Fi <-> mobile data) and re-run this
script — completed steps are skipped.
Or finish it by hand inside Ubuntu:
proot-distro login ubuntu
apt clean && rm -rf /var/lib/apt/lists/*
apt-get update -o Acquire::http::No-Cache=true
apt install -y curl git wget build-essential
curl -fsSL https://claude.ai/install.sh | bash
echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc && source ~/.bashrc
claude --version
See troubleshooting.md"
fi
# --- next steps ----------------------------------------------------------------
cat <<'EOF'
======================================================================
Claude Code is installed. Two manual steps left — both need your own
accounts, so they are not automated.
Same list, with checks and explanations:
README.md -> "After setup.sh finishes"
======================================================================
FIRST, know which shell you are in. This trips up most people:
Termux prompt: ~ $ -> 9router lives HERE
Ubuntu prompt: root@...:~# -> claude lives HERE
----------------------------------------------------------------------
1. 9ROUTER (in Termux, this session)
npm install -g 9router
9router
Leave it running. Open http://localhost:20128 in your phone browser.
Default dashboard password: 123456
>> CHANGE IT. It is a published default. Do this before you use this
phone on any shared or public network.
Add your free providers, then create a priority-ordered combo and
name it: claude-opus-free
----------------------------------------------------------------------
2. SETTINGS.JSON (inside Ubuntu, in a NEW Termux session)
Swipe from the left edge -> New session, then:
proot-distro login ubuntu
mkdir -p ~/.claude
nano ~/.claude/settings.json
This must be UBUNTU's home directory. A settings.json in Termux's
home is silently ignored — no error, nothing works. Claude Code runs
inside Ubuntu and only reads Ubuntu's home.
Config to paste: README.md, Chapter 6.
----------------------------------------------------------------------
THEN, daily use — two Termux sessions:
session 1: 9router
session 2: proot-distro login ubuntu -> claude
Stuck? -> troubleshooting.md
======================================================================
EOF