@znmin/claude-remote-cli 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/package.json +1 -1
- package/src/cli.js +6 -3
- package/src/local-mode.js +4 -1
- package/src/ssh-auth.js +18 -1
- package/src/ssh-policy.js +92 -0
package/README.md
CHANGED
|
@@ -25,10 +25,10 @@ npm install -g git+https://github.com/znmin/claude-remote-cli.git
|
|
|
25
25
|
也可以从已下载的分发包安装:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npm install -g ./znmin-claude-remote-cli-0.2.
|
|
28
|
+
npm install -g ./znmin-claude-remote-cli-0.2.3.tgz
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.
|
|
31
|
+
若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.3.tgz`,并确保 `~/.local/bin` 在 PATH 中;卸载时也使用相同的 `--prefix`。
|
|
32
32
|
|
|
33
33
|
npm 中不带作用域的 `claude-remote-cli` 属于其他项目。本项目使用独立的 `@znmin/claude-remote-cli` 作用域包名;请安装完整包名,或使用上述 GitHub 地址及本项目分发的 `.tgz` 文件。
|
|
34
34
|
|
|
@@ -81,7 +81,17 @@ claude --continue
|
|
|
81
81
|
claude-remote setup-ssh
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
0.2.3 起,首次密码登录后会先读取当前账号和连接对应的有效 sshd 策略。发现 `PubkeyAuthentication no` 或不允许单独使用公钥的 `AuthenticationMethods` 时,在生成/添加密钥之前停止,给出具体原因;无法读取策略时会说明限制并尝试普通公钥授权。
|
|
85
|
+
|
|
86
|
+
如果服务器关闭了公钥登录,可以显式运行:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
claude-remote setup-ssh --enable-pubkey
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
该选项只支持 root 登录的标准 Linux systemd SSH 服务(`ssh.service` 或 `sshd.service`),使用默认 `/etc/ssh/sshd_config`,不支持自定义 `-f`/`-o` 启动参数。它会备份原配置,只在全局配置最前面添加 `PubkeyAuthentication yes`,不改密码登录、root 登录或多因素策略;通过 `sshd -t` 和当前连接的有效策略校验后重新加载服务,随后继续添加公钥并独立验证登录。校验或加载失败会恢复备份并尝试重新加载原配置;备份路径会打印出来。普通启动不会自动修改服务策略。额外认证、Match 限制、容器/非 systemd 服务和自定义授权路径需要管理员处理。
|
|
93
|
+
|
|
94
|
+
非交互运行且免密连接不可用时会停止并提示先在终端完成设置,不自动修改密钥。服务器必须允许当前账号修改默认 `authorized_keys` 文件。已有带口令密钥如需免提示,可预先交给本机 SSH Agent 管理。
|
|
85
95
|
|
|
86
96
|
若需要使用特定的 Claude 可执行文件,可指定绝对路径;自定义路径不存在时会提示修正配置,不会自动改用其他程序:
|
|
87
97
|
|
|
@@ -132,7 +142,7 @@ Linux 上仍需安装 Playwright 所需的系统共享库,有窗口模式还
|
|
|
132
142
|
|
|
133
143
|
本地模式要求服务器允许 SSH TCP 转发,Claude Code 版本至少 **2.1.248**;已有版本过旧时会提示在服务器运行 `claude update`。每个会话创建仅监听本机回环地址的工具服务、随机令牌和 SSH 反向隧道;令牌通过 SSH 标准输入写入服务器 0600 临时配置,不出现在命令行参数或同步项目中。退出时清理配置、连接和浏览器。
|
|
134
144
|
|
|
135
|
-
启动时使用 `--restricted`、`--tools ""`、`--strict-mcp-config`,限定工具和设置来源、关闭普通 hooks
|
|
145
|
+
启动时使用 `--restricted`、`--tools ""`、`--strict-mcp-config`,限定工具和设置来源、关闭普通 hooks。0.2.2 起恢复 `/config`、`/model`、`/help` 等内置交互命令,不再使用禁用整个命令菜单的 `--disable-slash-commands`;通过 `disableSkillShellExecution` 和 `disableBundledSkills` 单独关闭技能内联 shell 和内置技能工作流。恢复菜单不代表所有依赖服务器项目目录或原生工具的命令都适用于本地模式。设置面板修改的是服务器上的 Claude 配置;受限模式仍只加载托管配置和本次 `--settings`,不会恢复读取普通用户/项目 settings.json,因此偏好设置的跨会话持久化仍受该限制影响,可用 `claude --model sonnet` 明确指定启动模型。保留服务器上的订阅认证,不使用跳过 OAuth 的 `--bare`。本地连接失效时不会退回服务器工具。支持 `--continue`、`--resume`、`--model`、`--effort`、`-p` 和输入/输出格式;暂不接受替换工具配置、加载插件、切换云会话等参数。
|
|
136
146
|
|
|
137
147
|
这是工具路由约束,不是对 Claude Code 主进程的系统隔离。认证、会话和日志仍在服务器,管理员管理策略继续生效。文件内容、命令结果和截图会经过服务器交给模型,不整体同步不代表数据不离开本机。
|
|
138
148
|
|
|
@@ -200,7 +210,7 @@ npm test
|
|
|
200
210
|
CLAUDE_REMOTE_INTEGRATION=1 CLAUDE_REMOTE_BROWSER_TEST=1 npm test
|
|
201
211
|
|
|
202
212
|
npm pack
|
|
203
|
-
npm install -g ./znmin-claude-remote-cli-0.2.
|
|
213
|
+
npm install -g ./znmin-claude-remote-cli-0.2.3.tgz
|
|
204
214
|
|
|
205
215
|
# 确定包名、版本和使用条款后,使用有发布权限的 npm 账号发布
|
|
206
216
|
npm login
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ const HELP = `${PACKAGE.name} ${PACKAGE.version}(非 Anthropic 官方客户端
|
|
|
20
20
|
claude-remote flush 等待当前目录同步完成
|
|
21
21
|
claude-remote stop [--all | --name 名称]
|
|
22
22
|
claude-remote doctor 检查本地环境,不连接服务器
|
|
23
|
-
claude-remote setup-ssh
|
|
23
|
+
claude-remote setup-ssh [--enable-pubkey] 配置免密登录;可显式开启服务器公钥认证
|
|
24
24
|
claude-remote install-sync 准备同步工具(通常自动完成)
|
|
25
25
|
claude-remote install-browser 准备本地受控浏览器(本地模式自动完成)
|
|
26
26
|
claude-remote run [Claude 参数] 启动远程 Claude
|
|
@@ -97,10 +97,13 @@ export async function main(asClaude, args = process.argv.slice(2), client = new
|
|
|
97
97
|
if (positionals.length || values.all && values.name) throw new Error('用法:claude-remote stop [--all | --name 名称]');
|
|
98
98
|
await client.stop(values); return 0;
|
|
99
99
|
}
|
|
100
|
-
if (rest.length) throw new Error(`${action} 不接受额外参数。`);
|
|
101
100
|
if (action === 'setup-ssh') {
|
|
102
|
-
|
|
101
|
+
const { values, positionals } = parseArgs({ args: rest, allowPositionals: true,
|
|
102
|
+
options: { 'enable-pubkey': { type: 'boolean' } } });
|
|
103
|
+
if (positionals.length) throw new Error('用法:claude-remote setup-ssh [--enable-pubkey]');
|
|
104
|
+
await client.ensureSSH(client, await client.settings(), { enablePubkey: Boolean(values['enable-pubkey']) }); return 0;
|
|
103
105
|
}
|
|
106
|
+
if (rest.length) throw new Error(`${action} 不接受额外参数。`);
|
|
104
107
|
if (action === 'status') {
|
|
105
108
|
await client.sync(['list', '--long', '--label-selector', 'claude-remote=true'], { capture: false }); return 0;
|
|
106
109
|
}
|
package/src/local-mode.js
CHANGED
|
@@ -39,7 +39,10 @@ export function validateLocalArguments(args) {
|
|
|
39
39
|
export function localClaudeArguments(configFile, root, extra) {
|
|
40
40
|
validateLocalArguments(extra);
|
|
41
41
|
return ['--restricted', '--tools', '', '--strict-mcp-config', '--mcp-config', configFile,
|
|
42
|
-
|
|
42
|
+
// Keep built-in UI commands (/config, /model, /help) available. Disable
|
|
43
|
+
// skill shell expansion separately instead of disabling the entire menu.
|
|
44
|
+
'--setting-sources', '', '--settings', JSON.stringify({ disableAllHooks: true,
|
|
45
|
+
disableSkillShellExecution: true, disableBundledSkills: true, enabledPlugins: {} }),
|
|
43
46
|
'--append-system-prompt', `All project operations run on the developer's LOCAL computer through the local MCP server. ` +
|
|
44
47
|
`The LOCAL project root is ${JSON.stringify(root)}. The server working directory is only for session metadata, not project files. ` +
|
|
45
48
|
'Call workspace_info before working. Use only local tools for file access, shell commands and browser operations. ' +
|
package/src/ssh-auth.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { sshBase } from './ssh.js';
|
|
6
6
|
import { exists, quote, which, withLock } from './system.js';
|
|
7
|
+
import { parseSSHPolicy, sshPolicyScript } from './ssh-policy.js';
|
|
7
8
|
|
|
8
9
|
// Force a new public-key connection: an existing multiplexed/password session
|
|
9
10
|
// must never be mistaken for successful key authentication.
|
|
@@ -21,7 +22,7 @@ export function authorizeKeyScript(publicKey) {
|
|
|
21
22
|
'printf "\\n%s\\n" "$key" >> "$HOME/.ssh/authorized_keys"; fi';
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export async function ensureSSH(client, config) {
|
|
25
|
+
export async function ensureSSH(client, config, { enablePubkey = false } = {}) {
|
|
25
26
|
if (!await which('ssh', client.env.PATH)) throw new Error('找不到系统 SSH 客户端。');
|
|
26
27
|
const run = (args, options = {}) => client.execute('ssh', [...sshBase(config), ...args],
|
|
27
28
|
{ env: client.env, timeout: 30000, ...options });
|
|
@@ -43,6 +44,22 @@ export async function ensureSSH(client, config) {
|
|
|
43
44
|
// Do not capture authentication output or handle passwords in JavaScript.
|
|
44
45
|
await run(['-M', '-S', socket, '-o', 'ControlPersist=no', '-o', 'ControlMaster=yes',
|
|
45
46
|
'-f', '-N', config.host], { timeout: 180000, capture: false });
|
|
47
|
+
const inspect = await run(['-S', socket, config.host, 'sh -c ' + quote(sshPolicyScript({ enable: enablePubkey }))],
|
|
48
|
+
{ capture: true });
|
|
49
|
+
const policy = parseSSHPolicy(inspect.stdout);
|
|
50
|
+
if (policy.backup) client.log('服务器公钥登录已开启,原配置备份:' + policy.backup);
|
|
51
|
+
if (policy.pubkeyauthentication === 'no')
|
|
52
|
+
throw new Error('服务器关闭了公钥登录(PubkeyAuthentication no),尚未生成或添加密钥。' +
|
|
53
|
+
(policy.uid === '0' ? '如需开启,请运行:claude-remote setup-ssh --enable-pubkey(备份配置、校验后重新加载,保留密码登录)。' :
|
|
54
|
+
'请联系管理员开启该账号的公钥登录,再运行 claude-remote setup-ssh。'));
|
|
55
|
+
if (policy.authenticationmethods && policy.authenticationmethods !== 'any' &&
|
|
56
|
+
!policy.authenticationmethods.split(/\s+/).includes('publickey'))
|
|
57
|
+
throw new Error('服务器 AuthenticationMethods=' + policy.authenticationmethods +
|
|
58
|
+
',不允许仅凭公钥登录;请联系管理员确认认证策略。未修改该策略,尚未生成或添加密钥。');
|
|
59
|
+
if (policy.status !== 'ok') {
|
|
60
|
+
if (enablePubkey) throw new Error('无法读取服务器有效 SSH 配置,未自动修改服务器策略;请联系管理员检查。');
|
|
61
|
+
client.log('当前账号无法读取 SSH 服务策略,将尝试普通公钥授权;如失败请由管理员检查公钥登录是否开启。');
|
|
62
|
+
}
|
|
46
63
|
const id = createHash('sha256').update(config.host + '\0' + (config.port ?? '')).digest('hex').slice(0, 20);
|
|
47
64
|
const key = path.join(directory, 'id_ed25519_' + id);
|
|
48
65
|
await fs.chmod(directory, 0o700);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { quote } from './system.js';
|
|
2
|
+
|
|
3
|
+
// configFile is injectable only for isolated filesystem tests, never a CLI flag.
|
|
4
|
+
export function sshPolicyScript({ enable = false, configFile = '/etc/ssh/sshd_config', procRoot = '/proc' } = {}) {
|
|
5
|
+
return `# claude-remote-ssh-policy
|
|
6
|
+
set -eu
|
|
7
|
+
sshd=$(command -v sshd || true)
|
|
8
|
+
if [ -z "$sshd" ] && [ -x /usr/sbin/sshd ]; then sshd=/usr/sbin/sshd; fi
|
|
9
|
+
printf 'uid=%s\\n' "$(id -u)"
|
|
10
|
+
if [ -z "$sshd" ]; then printf 'status=unavailable\\n'; exit 0; fi
|
|
11
|
+
set -- \${SSH_CONNECTION:-}
|
|
12
|
+
connection="user=$(id -un)"
|
|
13
|
+
if [ "$#" -eq 4 ]; then connection="$connection,addr=$1,host=$1,laddr=$3,lport=$4"; fi
|
|
14
|
+
policy() { "$sshd" -T -C "$connection"; }
|
|
15
|
+
if ! current=$(policy 2>/dev/null); then printf 'status=unavailable\\n'; exit 0; fi
|
|
16
|
+
${enable ? `
|
|
17
|
+
if printf '%s\\n' "$current" | grep -qx 'pubkeyauthentication no'; then
|
|
18
|
+
printf '%s\\n' "$current" | grep -Eq '^authenticationmethods (any|([^ ]+ )*publickey)( |$)' || {
|
|
19
|
+
echo '服务器要求额外认证,未修改认证策略。' >&2; exit 1;
|
|
20
|
+
}
|
|
21
|
+
[ "$(id -u)" = 0 ] || { echo '自动开启公钥登录需要 root;请由管理员配置。' >&2; exit 1; }
|
|
22
|
+
[ "$(uname -s)" = Linux ] || { echo '自动开启仅支持 Linux systemd 的标准 SSH 服务。' >&2; exit 1; }
|
|
23
|
+
service=
|
|
24
|
+
for name in ssh.service sshd.service; do
|
|
25
|
+
if systemctl is-active --quiet "$name"; then service=$name; break; fi
|
|
26
|
+
done
|
|
27
|
+
[ -n "$service" ] || { echo '无法确认正在运行的 SSH 服务,未修改配置。' >&2; exit 1; }
|
|
28
|
+
pid=$(systemctl show "$service" --property=MainPID --value)
|
|
29
|
+
case "$pid" in ''|*[!0-9]*|0) echo '无法确认 SSH 服务进程,未修改配置。' >&2; exit 1;; esac
|
|
30
|
+
process_file=${quote(procRoot)}/"$pid"/cmdline
|
|
31
|
+
[ -r "$process_file" ] || { echo '无法读取 SSH 服务参数,未修改配置。' >&2; exit 1; }
|
|
32
|
+
process_args=$(tr '\\000' ' ' < "$process_file")
|
|
33
|
+
invocation=$(systemctl show "$service" --property=ExecStart --property=Environment)
|
|
34
|
+
case "$invocation $process_args" in
|
|
35
|
+
*' -f'*|*' -o'*) echo 'SSH 服务使用自定义参数,请由管理员配置;未修改配置。' >&2; exit 1;;
|
|
36
|
+
esac
|
|
37
|
+
file=${quote(configFile)}
|
|
38
|
+
[ -f "$file" ] && [ ! -L "$file" ] || { echo 'SSH 配置不是普通文件,未修改配置。' >&2; exit 1; }
|
|
39
|
+
backup=$(mktemp "$file.claude-remote.XXXXXX")
|
|
40
|
+
cp -p "$file" "$backup"
|
|
41
|
+
changed=0
|
|
42
|
+
candidate=
|
|
43
|
+
rollback() {
|
|
44
|
+
code=$?
|
|
45
|
+
trap - EXIT HUP INT TERM
|
|
46
|
+
if [ -n "$candidate" ]; then rm -f "$candidate"; fi
|
|
47
|
+
if [ "$changed" = 1 ]; then
|
|
48
|
+
cp -p "$backup" "$file"
|
|
49
|
+
if ! "$sshd" -t || ! systemctl reload "$service"; then
|
|
50
|
+
echo "恢复后的 SSH 配置校验或重新加载失败,请使用备份检查:$backup" >&2
|
|
51
|
+
fi
|
|
52
|
+
echo "开启失败,已恢复原配置;备份:$backup" >&2
|
|
53
|
+
fi
|
|
54
|
+
exit "$code"
|
|
55
|
+
}
|
|
56
|
+
trap rollback EXIT
|
|
57
|
+
trap 'exit 1' HUP INT TERM
|
|
58
|
+
# First global value wins, including over later Include files. Restrictive
|
|
59
|
+
# Match rules must pass the connection-specific check or we roll back.
|
|
60
|
+
candidate=$(mktemp "$file.claude-remote-stage.XXXXXX")
|
|
61
|
+
cp -p "$backup" "$candidate"
|
|
62
|
+
{ printf 'PubkeyAuthentication yes\\n'; cat "$backup"; } > "$candidate"
|
|
63
|
+
changed=1
|
|
64
|
+
mv -f "$candidate" "$file"
|
|
65
|
+
"$sshd" -t
|
|
66
|
+
current=$(policy)
|
|
67
|
+
printf '%s\\n' "$current" | grep -qx 'pubkeyauthentication yes' || {
|
|
68
|
+
echo '当前账号的 Match 策略仍禁止公钥登录。' >&2; exit 1;
|
|
69
|
+
}
|
|
70
|
+
systemctl reload "$service"
|
|
71
|
+
changed=0
|
|
72
|
+
trap - EXIT HUP INT TERM
|
|
73
|
+
printf 'backup=%s\\n' "$backup"
|
|
74
|
+
fi
|
|
75
|
+
` : ''}
|
|
76
|
+
printf 'status=ok\\n'
|
|
77
|
+
printf '%s\\n' "$current" | while IFS=' ' read -r key value; do
|
|
78
|
+
case "$key" in pubkeyauthentication|authenticationmethods|authorizedkeysfile|permitrootlogin)
|
|
79
|
+
printf '%s=%s\\n' "$key" "$value";;
|
|
80
|
+
esac
|
|
81
|
+
done
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseSSHPolicy(output) {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const line of output.split(/\r?\n/)) {
|
|
88
|
+
const index = line.indexOf('=');
|
|
89
|
+
if (index > 0) result[line.slice(0, index)] = line.slice(index + 1);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|