Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.tgz
/.verify-*/
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ config/claude-termux-release-manifest.json
packages/claude-code/config/claude-termux-release-manifest.json
```

## Known Limitations / 既知の制限

**English:** Remote Control (controlling a session from claude.ai/code or the Claude mobile app) does not work in this Termux wrapper. The wrapper replaces the `ws` (WebSocket) module with a no-op stub, so the underlying connection Remote Control needs is never established. The TUI may show `Remote Control disconnected` on startup; this is expected and does not affect normal interactive use, `-p` (print mode), or other CLI commands.

**日本語:** このTermuxラッパーでは Remote Control(claude.ai/code や Claude モバイルアプリからのセッション操作)は動作しません。ラッパーが `ws`(WebSocket)モジュールを何もしない no-op スタブに置き換えているため、Remote Control が必要とする接続が確立されません。TUI 起動時に `Remote Control disconnected` と表示されることがありますが、これは想定内の挙動であり、通常の対話操作・`-p`(print mode)・その他の CLI コマンドには影響しません。

## Verify / 確認

```sh
Expand Down
8 changes: 8 additions & 0 deletions config/claude-native-audited-versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,14 @@
"tarball_integrity": "sha512-tHsJhjwcoFyKTCQfYCclKjLtbnbbGKvcrzxUhMb6NDlPw9RrawmjSWUAdla7PtEVPaJsGkjGo4O2MQTlYIT+PA==",
"tarball_sha256": "41de896e672667bc5b5060419d5a1ff5cfe15b77659d1418b10df5095e38419a",
"status": "termux_verified"
},
"2.1.245": {
"wrapper_spec": "@anthropic-ai/[email protected]",
"native_spec": "@anthropic-ai/[email protected]",
"entry_format": "esm-chunked",
"tarball_integrity": "sha512-Qbn5HnZbYeW4GdifVkDGfcVKqj3/f3U9sfVd3LEaUZh08CcS8D/ptJ76zuBfQUGHJHfToAdNVfak86uJ84b1Dg==",
"tarball_sha256": "668662e7b5d91a93cff6c75736e60f3d5d3bed4cbe077ae4feefc63ad1253f4d",
"status": "offset_discovered"
}
}
}
2 changes: 1 addition & 1 deletion config/claude-termux-release-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 1,
"package_name": "@bash0816/claude-code",
"latest_audited_version": "2.1.241",
"latest_candidate_version": "2.1.241",
"latest_candidate_version": "2.1.245",
"previous_stable_version": "2.1.240",
"stable_pinned_version": "2.1.220-2",
"manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json"
Expand Down
12 changes: 10 additions & 2 deletions packages/claude-code/bin/claude
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,18 @@ process.stdout.write(String(item[field]));
' "${CONFIG_FILE}" "${CLAUDE_VERSION}" "$1"
}

ENTRY_JS_OFFSET=$(json_field entry_js_offset)
ENTRY_END_OFFSET=$(json_field entry_end_offset)
ENTRY_FORMAT=$(json_field entry_format || echo "legacy-cjs")
CACHE_DIR="${CLAUDE_TERMUX_PACKAGE_CACHE:-${HOME}/.claude-termux-native-package}/versions/${CLAUDE_VERSION}"
SOURCE_BIN="${CACHE_DIR}/app/node_modules/@anthropic-ai/claude-code-linux-arm64/claude"

if [ "${ENTRY_FORMAT}" = "esm-chunked" ]; then
ENTRY_JS_OFFSET=""
ENTRY_END_OFFSET=""
else
ENTRY_JS_OFFSET=$(json_field entry_js_offset)
ENTRY_END_OFFSET=$(json_field entry_end_offset)
fi

if [ ! -f "${SOURCE_BIN}" ]; then
"$NODE" "${PACKAGE_DIR}/lib/prepare-native.js" "${CLAUDE_VERSION}"
fi
Expand All @@ -58,6 +65,7 @@ fi

SOURCE_BIN="${SOURCE_BIN}" \
WORKDIR="${CACHE_DIR}/launcher-workdir" \
ENTRY_FORMAT="${ENTRY_FORMAT}" \
ENTRY_JS_OFFSET="${ENTRY_JS_OFFSET}" \
ENTRY_END_OFFSET="${ENTRY_END_OFFSET}" \
CURRENT_CLAUDE_VERSION="${CLAUDE_VERSION}" \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,14 @@
"tarball_integrity": "sha512-tHsJhjwcoFyKTCQfYCclKjLtbnbbGKvcrzxUhMb6NDlPw9RrawmjSWUAdla7PtEVPaJsGkjGo4O2MQTlYIT+PA==",
"tarball_sha256": "41de896e672667bc5b5060419d5a1ff5cfe15b77659d1418b10df5095e38419a",
"status": "termux_verified"
},
"2.1.245": {
"wrapper_spec": "@anthropic-ai/[email protected]",
"native_spec": "@anthropic-ai/[email protected]",
"entry_format": "esm-chunked",
"tarball_integrity": "sha512-Qbn5HnZbYeW4GdifVkDGfcVKqj3/f3U9sfVd3LEaUZh08CcS8D/ptJ76zuBfQUGHJHfToAdNVfak86uJ84b1Dg==",
"tarball_sha256": "668662e7b5d91a93cff6c75736e60f3d5d3bed4cbe077ae4feefc63ad1253f4d",
"status": "offset_discovered"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 1,
"package_name": "@bash0816/claude-code",
"latest_audited_version": "2.1.241",
"latest_candidate_version": "2.1.241",
"latest_candidate_version": "2.1.245",
"previous_stable_version": "2.1.240",
"stable_pinned_version": "2.1.220-2",
"manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json"
Expand Down
18 changes: 18 additions & 0 deletions packages/claude-code/lib/bunfs-child-process-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const realChildProcess = require('node:child_process');
const { createGuardedChildProcess } = require('./native-update-guard.js');
const guarded = createGuardedChildProcess(realChildProcess, (v) => process.stderr.write(v));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Termux URL rewriting in the ESM child-process guard

When the ESM CLI opens a browser during authentication or another URL-opening flow, this guard forwards xdg-open unchanged to the real child-process module, but Termux provides termux-open-url instead. The legacy path explicitly rewrites xdg-open in both spawn and spawnSync, and the launcher still requires termux-open-url; therefore the new esm-chunked path regresses those flows unless the same rewrite is applied before creating the guarded methods.

Useful? React with 👍 / 👎.


export const spawn = guarded.spawn;
export const execFile = guarded.execFile;
export const exec = guarded.exec;
export const spawnSync = guarded.spawnSync;
export const execFileSync = guarded.execFileSync;
export const execSync = guarded.execSync;
export const ChildProcess = realChildProcess.ChildProcess;
export const fork = realChildProcess.fork;
export const _forkChild = realChildProcess._forkChild;

export default Object.assign({}, realChildProcess, guarded);
113 changes: 113 additions & 0 deletions packages/claude-code/lib/bunfs-child-process-guard.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');

test('bunfs-child-process-guard exports named exports for guarded methods', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');

assert.equal(typeof guard.spawn, 'function');
assert.equal(typeof guard.execFile, 'function');
assert.equal(typeof guard.exec, 'function');
assert.equal(typeof guard.spawnSync, 'function');
assert.equal(typeof guard.execFileSync, 'function');
assert.equal(typeof guard.execSync, 'function');
});

test('bunfs-child-process-guard exports ChildProcess, fork, _forkChild', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');

// ChildProcess is a class
assert.equal(typeof guard.ChildProcess, 'function');
// fork and _forkChild are functions
assert.equal(typeof guard.fork, 'function');
assert.equal(typeof guard._forkChild, 'function');
});

test('bunfs-child-process-guard provides default export with all methods', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');
const defaultExport = guard.default;

assert.ok(defaultExport);
assert.equal(typeof defaultExport, 'object');
assert.equal(typeof defaultExport.spawn, 'function');
assert.equal(typeof defaultExport.execFile, 'function');
assert.equal(typeof defaultExport.exec, 'function');
assert.equal(typeof defaultExport.spawnSync, 'function');
assert.equal(typeof defaultExport.execFileSync, 'function');
assert.equal(typeof defaultExport.execSync, 'function');
assert.equal(typeof defaultExport.ChildProcess, 'function');
assert.equal(typeof defaultExport.fork, 'function');
assert.equal(typeof defaultExport._forkChild, 'function');
});

test('bunfs-child-process-guard blocks official package update via execFileSync', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');

// Try to execute npm install of official package
try {
guard.execFileSync('npm', ['install', '-g', '@anthropic-ai/claude-code@latest']);
// If it doesn't throw, that's an error (should be blocked)
assert.fail('Expected execFileSync to block official package update');
} catch (err) {
// Should throw with CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED code
assert.ok(
err.code === 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED' ||
err.message.includes('disabled on Termux'),
`Expected block error, got: ${err.message}`,
);
}
});

test('bunfs-child-process-guard blocks official package update via execSync', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');

// Try to execute npm install via exec
try {
guard.execSync('npm install -g @anthropic-ai/claude-code@latest');
// If it doesn't throw, that's an error (should be blocked)
assert.fail('Expected execSync to block official package update');
} catch (err) {
// Should throw with CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED code
assert.ok(
err.code === 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED' ||
err.message.includes('disabled on Termux'),
`Expected block error, got: ${err.message}`,
);
}
});

test('bunfs-child-process-guard allows harmless commands', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');

// echo is a harmless command and should not be blocked
const result = guard.execFileSync('echo', ['hello']);
// Verify the command actually executed and produced output
assert.equal(result.toString().trim(), 'hello');
});

test('bunfs-child-process-guard spawn blocks official package install', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');

// spawn should return a blocked child process (EventEmitter-like)
const child = guard.spawn('npm', ['install', '-g', '@anthropic-ai/claude-code']);

// Blocked spawn should have specific properties
assert.equal(child.stdout, null);
assert.equal(child.stderr, null);
assert.equal(child.stdin, null);
assert.equal(child.pid, 0);
assert.equal(child.killed, false);

// Verify it's event-like (has on method or can be used as event emitter)
assert.equal(typeof child.kill, 'function');
});

test('bunfs-child-process-guard does not block other package installs', async () => {
const guard = await import('./bunfs-child-process-guard.mjs');

// Installing a different package should not be blocked
const result = guard.execFileSync('echo', ['@bash0816/claude-code']);
// Verify the command actually executed and produced correct output
assert.equal(result.toString().trim(), '@bash0816/claude-code');
});
74 changes: 74 additions & 0 deletions packages/claude-code/lib/bunfs-esm-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { pathToFileURL, fileURLToPath } from 'node:url';
import { existsSync, readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';

let PROCESS_OWNED_DIR = null;
let SOURCE_BIN = null;
let CHILD_PROCESS_GUARD_PATH = null;
let VM_GUARD_PATH = null;
let WS_STUB_PATH = null;

export function initialize(data) {
PROCESS_OWNED_DIR = data.processOwnedDir;
SOURCE_BIN = data.sourceBin;
CHILD_PROCESS_GUARD_PATH = data.childProcessGuardPath;
VM_GUARD_PATH = data.vmGuardPath;
WS_STUB_PATH = data.wsStubPath;
}

function buildImportMetaRequirePolyfillPrelude(anchorUrl) {
return (
`import __bunfsGuardedChildProcess from ${JSON.stringify(pathToFileURL(CHILD_PROCESS_GUARD_PATH).href)};\n` +
`import __bunfsGuardedVm from ${JSON.stringify(pathToFileURL(VM_GUARD_PATH).href)};\n` +
`import { createRequire as __bunfsCreateRequire } from "node:module";\n` +
`const __bunfsRealRequire = __bunfsCreateRequire(${JSON.stringify(anchorUrl)});\n` +
`const __bunfsMetaRequire = (id) => {\n` +
` if (id === "child_process" || id === "node:child_process") return __bunfsGuardedChildProcess;\n` +
` if (id === "vm" || id === "node:vm") return __bunfsGuardedVm;\n` +
` return __bunfsRealRequire(id);\n` +
`};\n`
);
}

export async function resolve(specifier, context, nextResolve) {
if (specifier === 'child_process' || specifier === 'node:child_process') {
return { url: pathToFileURL(CHILD_PROCESS_GUARD_PATH).href, shortCircuit: true, format: 'module' };
}
if (specifier === 'vm' || specifier === 'node:vm') {
return { url: pathToFileURL(VM_GUARD_PATH).href, shortCircuit: true, format: 'module' };
}
if (specifier === 'ws') {
return { url: pathToFileURL(WS_STUB_PATH).href, shortCircuit: true, format: 'module' };
}
if (specifier.startsWith('/$bunfs/root/')) {
const rel = specifier.slice('/$bunfs/root/'.length);
if (rel.includes('..') || path.isAbsolute(rel)) {
throw new Error(`bunfs resolve: rejected specifier ${specifier}`);
}
const real = path.resolve(PROCESS_OWNED_DIR, rel);
if (path.relative(PROCESS_OWNED_DIR, real).startsWith('..')) {
throw new Error(`bunfs resolve: path escapes process-owned dir: ${specifier}`);
}
if (!existsSync(real)) {
throw new Error(`bunfs resolve: missing extracted module ${specifier} -> ${real}`);
}
return { url: pathToFileURL(real).href, shortCircuit: true, format: 'module' };
}
return nextResolve(specifier, context);
}

export async function load(url, context, nextLoad) {
const ownedPrefix = pathToFileURL(PROCESS_OWNED_DIR + path.sep).href;
if (!url.startsWith(ownedPrefix)) {
return nextLoad(url, context);
}
const filePath = fileURLToPath(url);
let source = readFileSync(filePath, 'utf8');
if (source.includes('import.meta.require')) {
const anchorUrl = pathToFileURL(SOURCE_BIN).href;
source = buildImportMetaRequirePolyfillPrelude(anchorUrl) +
source.replaceAll('import.meta.require', '__bunfsMetaRequire');
}
return { format: 'module', source, shortCircuit: true };
}
Loading