Shell 脚本质量审核报告
审核日期:2026-04-07
审核范围:WorkPod Alpine(含 Test 变体)全部 Shell 脚本
审核人:Shell 脚本专家
脚本清单
| 脚本 |
行数 |
用途 |
复杂度 |
| entrypoint.sh |
81 |
Alpine 版容器入口,启动 SSH + ttyd + 开发工具检测 |
中 |
| entrypoint-test.sh |
129 |
Test 容器入口,额外创建 developer 用户 + Claude 全权限配置 |
中高 |
| ttyd-session.sh |
63 |
tmux 会话管理,URL 参数解析 + 交互式菜单 |
中 |
| 合计 |
273 |
|
|
逐脚本审核
entrypoint.sh(Alpine 版)
文件路径: E:/wk-lab/workpod/entrypoint.sh
健壮性
| # |
问题 |
严重度 |
说明 |
| R1 |
SSHD_PID 未赋值(与 Ubuntu 版相同的问题) |
高 |
cleanup() 中 kill "$SSHD_PID" 因 PID 始终为 0 而跳过。/usr/sbin/sshd 在第42行前台启动(无 &),实际上 sshd 作为守护进程运行后返回,但未捕获其 PID。应改为 /usr/sbin/sshd & SSHD_PID=$! 或在 cleanup 中用 pkill sshd。 |
| R2 |
sshd 未使用 -D 前台模式 |
中 |
第42行直接调用 /usr/sbin/sshd,它会 fork 为守护进程。如果后续命令失败导致脚本退出,sshd 会成为孤儿进程。建议用 -D 参数保持前台或正确追踪 PID。 |
| R3 |
sleep 1 硬编码等待时间 |
低 |
与 Ubuntu 版相同,仅等待 1 秒判断 ttyd 存活。慢速系统上可能不够。 |
| R4 |
dev-tools.sh 中的 ls -d 可能匹配多个目录 |
低 |
第38行 ls -d /root/rust-*/bin 如果存在多个 rust 版本目录,head -1 只取第一个。行为可预测但隐式依赖排序。 |
| R5 |
cleanup 中 wait 无参数 |
低 |
第11行 wait 2>/dev/null 等待所有后台子进程。由于只跟踪了 TTYD_PID 和 SSHD_PID(后者实际为 0),行为基本正确但语义不精确。 |
可移植性
| # |
问题 |
严重度 |
说明 |
| P1 |
shebang 使用 bash 但环境为 Alpine/musl |
中 |
#!/bin/bash 要求安装 bash 包。Dockerfile 第31行已包含 bash 依赖,所以当前可工作。但如果未来精简镜像可能出问题。脚本中未使用任何 bashism(数组、[[ ]] 等),理论上可改用 #!/bin/sh。 |
| P2 |
pidof 替代 pgrep |
正面 |
第55行使用 pidof sshd 而非 pgrep -x sshd,这是正确的 POSIX 兼容选择,在 BusyBox 环境下可靠工作。 |
| P3 |
gvm source 命令 |
低 |
dev-tools.sh 第34行 source /root/gvm/scripts/gvm 依赖 gvm 的初始化脚本格式。如果 gvm 不存在该路径会静默失败(有 2>/dev/null)。 |
安全性
| # |
问题 |
严重度 |
说明 |
| S1 |
ttyd 密码过于简单 ada:123 |
[已改进] |
原第45行 -c ada:123 已改为从 ${TTYD_CREDENTIALS:-jc:1234567} 环境变量读取,不再硬编码在脚本中。密码强度可通过环境变量灵活配置。 |
| S2 |
硬编码密码明文出现在脚本和日志中 |
[已改进] |
同上,密码已改为通过环境变量 TTYD_CREDENTIALS 传入,不再明文出现在脚本源码中。 |
| S3 |
dev-tools.sh 使用单引号 heredoc(安全) |
正面 |
第32行 << 'PROFILE' 阻止变量展开,防止注入。这是正确的做法。 |
| S4 |
无用户隔离 |
信息 |
Alpine 默认版直接以 root 运行一切,没有像 Test 版那样创建 developer 用户。作为开发环境可接受。 |
代码质量
| # |
问题 |
严重度 |
说明 |
| Q1 |
开发工具检测逻辑完善 |
正面 |
覆盖 cargo/rustup/gvm/pyenv/go/rust独立安装/local bin 共 7 种开发工具路径。 |
| Q2 |
双轨 PATH 设置 |
正面 |
既在当前 shell 设置 export(第19-29行),又写入 /etc/profile.d/dev-tools.sh(第32-40行)确保新会话生效。设计合理。 |
| Q3 |
启动信息展示完整 |
正面 |
展示 Node/npm/Claude/Rust/Go/Python 版本 + 连接方式,便于快速确认环境状态。 |
| Q4 |
版本检测容错好 |
正面 |
所有 --version 命令都有 2>/dev/null || echo '未安装' 保护。 |
| Q5 |
cleanup 函数与 Ubuntu 版完全重复 |
低 |
cleanup/trap 模式在三份 entrypoint 中复制粘贴。 |
代码质量评分:7.5 / 10
扣分项:SSHD_PID 未赋值(-1)、密码过弱(-0.5)、sshd 前台模式(-0.5)、代码重复(-0.5)
entrypoint-test.sh(Test 容器入口) [归档]
此文件已移至 _archive/,以下问题仅作历史记录。
文件路径: E:/wk-lab/workpod-alpine/entrypoint-test.sh(已归档至 _archive/)
健壮性
| # |
问题 |
严重度 |
说明 |
| R1 |
SSHD_PID 未赋值(继承自 Alpine 版) |
高 |
同 entrypoint.sh 的 R1 问题。 |
| R2 |
adduser -D 无密码设置 |
中 |
第32行 adduser -D -s /bin/bash developer 创建用户时未设置密码。developer 用户只能通过 sudo 操作,不能直接 SSH 登录(除非配置了密钥认证)。这可能是故意的,但应明确注释说明。 |
| R3 |
sudoers 文件写入无原子性保护 |
低 |
第34行直接 echo ... > /etc/sudoers.d/developer。如果脚本在中途被中断,可能留下不完整的 sudoers 文件导致 sudo 不可用。建议先写临时文件再 mv。 |
| R4 |
chown -R 递归操作范围大 |
低 |
第37行 chown -R developer:developer /home/developer 对整个 home 目录递归修改所有权。如果有其他进程正在向该目录写入文件,可能出现竞态。但在容器启动阶段风险极低。 |
| R5 |
settings.json 双引号 heredoc 安全 |
正面 |
第41行和第51行都使用 << 'SETTINGS' 单引号 heredoc,JSON 内容不会被 shell 解释。做法正确。 |
可移植性
| # |
问题 |
严重度 |
说明 |
| P1 |
adduser -D 是 Alpine 特有 |
信息 |
-D 标志表示"不分配密码",是 BusyBox adduser 的扩展。本脚本专用于 Alpine,无移植需求。 |
| P2 |
bash 依赖同 Alpine 版 |
中 |
同 entrypoint.sh 的 P1 问题。 |
安全性
| # |
问题 |
严重度 |
说明 |
| S1 |
Claude Code settings.json 全权限 allow:["*"] |
严重 [归档] |
第43-47行和第53-57行为 root 和 developer 用户都设置了 "allow": ["*"],意味着 Claude Code 可以执行任意文件读写、命令执行等操作而无需用户确认。虽然这是开发环境的故意设计(沙箱用途),但应在文档中明确标注此安全策略的影响范围。归档文件中的配置,不影响当前版本。 |
| S2 |
.bashrc 中环境变量传递方式 |
中 |
第67-70行使用双引号 heredoc << BASHRC 并在其中引用 ${ANTHROPIC_AUTH_TOKEN} 等变量。这里变量会在 heredoc 写入时展开——即使用 entrypoint 进程的环境变量值。这意味着:(1) 如果环境变量未设置,会写入空字符串;(2) 如果值包含特殊字符(如 $`` ),可能被二次解释。当前用法基本安全但需注意边界情况。 |
| S3 |
NOPASSWD:ALL sudo 权限 |
高 [归档] |
第34行给 developer 用户无密码完整 sudo 权限。配合 Claude Code 全权限配置,developer 用户等同于 root。这是有意的设计决策(全权限沙箱),但安全边界完全消失。同上,归档文件中的配置。 |
| S4 |
.bashrc 中 PATH 包含 $PATH |
正面 |
第66行 export PATH="/usr/local/bin:/usr/bin:/bin:\$PATH" 使用 \$PATH 在双引号 heredoc 中转义 $,确保写入文件的是字面量 $PATH 而非展开后的值。这个转义是正确的。 |
代码质量
| # |
问题 |
严重度 |
说明 |
| Q1 |
功能模块划分清晰 |
正面 |
按顺序分为:开发工具检测 -> 用户创建 -> Claude 配置(root) -> Claude 配置(developer) -> 别名配置(root) -> 启动服务 -> 信息展示。结构清晰。 |
| Q2 |
root 和 developer 配置对称处理 |
正面 |
settings.json 和 .bashrc 分别为两个用户配置,且 chown 所有权正确。 |
| Q3 |
alias 区分 root/developer 参数 |
正面 |
root 用 --allow-dangerously-skip-permissions(第76行),developer 用 --dangerously-skip-permissions(第65行)。准确反映了 Claude Code 对 root 用户的限制。 |
| Q4 |
与 entrypoint.sh (Alpine) 大量重复 |
高 [归档] |
开发工具检测(第19-29行 vs 第19-29行完全相同)、dev-tools.sh 写入(第80-88行 vs 第32-40行完全相同)、cleanup 函数(第7-14行相同)、服务启动和检查(第90-106行几乎相同)。唯一差异是用户创建和 Claude 配置部分(第31-78行)。重复率约 65%。因归档而不再是活跃问题。 |
| Q5 |
环境变量默认值处理良好 |
正面 |
第69-70行 ${API_TIMEOUT_MS:-3000000} 和 ${CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:-1} 提供了合理的默认值。 |
| Q6 |
端口/标题/密码硬编码 |
低 |
7682、2223、"WorkPod Test"、jc:1234567 全部硬编码。(注:此为已归档的 entrypoint-test.sh 中的历史值;当前基础实例已使用 2222/7681 端口,凭据通过环境变量注入) |
代码质量评分:7 / 10 (历史评分,文件已归档)
扣分项:与 entrypoint.sh 高度重复(-1.5)、SSHD_PID 未赋值(-1)、全权限安全策略未文档化(-0.5)
ttyd-session.sh(tmux 会话管理)
文件路径: E:/wk-lab/workpod/ttyd-session.sh
健壮性
| # |
问题 |
严重度 |
说明 |
| R1 |
set -e 缺失 |
中 |
脚本没有 set -e。如果中间某条命令失败(如 tmux list-sessions),脚本会继续执行可能导致错误状态扩散。考虑到脚本的交互性质(需要 read 输入),set -euo pipefail 更合适但需注意 read 在 set -e 下的行为(read 到 EOF 返回非零不会终止脚本,因为它是条件上下文的一部分)。 |
| R2 |
SESSION_NAME 初始为空字符串 |
低 |
第4行 SESSION_NAME="" 初始化为空。如果 TTYD_QUERY_STRING 解析也得到空值(第9行的 grep 无匹配),则进入交互菜单分支。流程正确但变量生命周期不够明确。 |
| R3 |
tmux session 名过滤后可能为空 |
中 |
第55行 tr -cd 'a-zA-Z0-9_\-' 过滤后如果 SESSION_NAME 变成空字符串(例如原始输入全是特殊字符),第58行 tmux new-session -t "" 会创建一个名为空字符串的 session。虽然 tmux 允许这样做但不推荐。应在过滤后检查是否为空并赋予默认值 "default"。 |
| R4 |
sed -n "${CHOICE}p" 未做范围校验 |
中 |
第45行 sed -n "${CHOICE}p" 直接将用户输入的数字传给 sed。虽然前面有 grep -qE '^[0-9]+$' 校验了纯数字,但没有检查数字是否在有效范围内(1 到 session 数量之间)。超出范围的 CHOICE 会导致 sed 输出空行,SESSION_NAME 被设为空字符串,然后触发 R3 的问题。 |
| R5 |
EXISTING 变量的 word splitting |
低 |
第20行 tmux list-sessions 输出类似 my_session: (attached) 格式,awk 提取第一列后得到 my_session:(带冒号)。第24行 for s in $EXISTING 依赖 word splitting 来遍历 session 名。如果 session 名包含空格(虽然 tmux 不允许),会出错。实际风险低。 |
| R6 |
exec tmux attach/new 替换进程 |
正面 |
第59-61行使用 exec 替换当前 shell 进程为 tmux,避免多余的 shell 进程残留。这是正确的做法。 |
可移植性
| # |
问题 |
严重度 |
说明 |
| P1 |
BusyBox awk 兼容性(已修复) |
已解决 |
历史上使用 grep -oP(Perl 正则)解析 query string,在 BusyBox 中不可用。当前版本第9行改用 tr '\&' '\n' | grep '^session=' | cut -d= -f2 的管道链,完全兼容 BusyBox。 |
| P2 |
tr -cd 字符类兼容性 |
低 |
第55行 tr -cd 'a-zA-Z0-9_\-' 中的 \- 在 GNU tr 和 BusyBox tr 中都能正确解释为字面量连字符。POSIX 标准中字符类内的 - 放在首位或末位或转义均可。 |
| P3 |
printf 格式化 |
正面 |
第25行 printf "║ [%d] %-33s║\n" "$I" "$s" 使用标准 printf 格式,跨实现兼容。 |
| P4 |
clear 命令 |
信息 |
第51行 clear 依赖 terminfo 数据库。在 Docker 的 ttyd 环境中通常可用。 |
| P5 |
grep -qE |
正面 |
第44行使用 grep -qE(扩展正则),BusyBox awk 支持此选项。 |
安全性
| # |
问题 |
严重度 |
说明 |
| S1 |
TTYD_QUERY_STRING 注入防护 |
正面 |
第9行通过管道链 tr | grep | cut 解析,最终结果经过第55行 tr -cd 过滤为安全字符集。即使攻击者构造恶意的 query string(如 session=;rm -rf /),也会被过滤掉。安全性良好。 |
| S2 |
用户输入 SESSION_NAME 注入防护 |
正面 |
第55行将用户交互输入同样通过 tr -cd 过滤,tmux session 名被限制在 [a-zA-Z0-9_-] 字符集中。无法注入 shell 命令。 |
| S3 |
无外部命令拼接 |
正面 |
所有变量传递给 tmux 时都加了双引号("$SESSION_NAME"),防止 word splitting 和 globbing。 |
代码质量
| # |
问题 |
严重度 |
说明 |
| Q1 |
UI 设计精美 |
正面 |
使用 Unicode 制表符绘制边框,视觉效果专业。编号选择 + 自定义名称的双重输入方式用户体验好。 |
| Q2 |
双模式设计合理 |
正面 |
URL 带 ?session=xxx 直接进入(自动化场景),不带参数显示菜单(手动场景)。覆盖了两种主要使用方式。 |
| Q3 |
默认值 fallback |
正面 |
第39-41行用户输入空时默认为 "default"。 |
| Q4 |
缺少 set -e/o pipefail |
低 |
如 R1 所述。对于这种交互式脚本,建议至少加 set -uo pipefail(不含 -e 以免影响 read)。 |
| Q5 |
冒号未从 session 名中去除 |
低 |
第20行 awk {print $1} 提取的是 session_name:(带尾部冒号)。这个带冒号的名字会被用于显示(第25行)和选择(第45行 sed 取行),最终传入 tmux 的也是带冒号的名字。tmux 本身允许 session 名包含冒号,所以功能不受影响,但显示上不够干净。可以用 tr -d ':' 清理。 |
| Q6 |
代码简洁高效 |
正面 |
仅 63 行实现了 query string 解析、交互式菜单、session 创建/附加的完整功能。没有冗余逻辑。 |
代码质量评分:8 / 10
扣分项:缺少 set -euo pipefail(-0.5)、session 名空值/超范围未防护(-1)、冒号未清理(-0.5)
跨脚本问题
| # |
问题 |
影响范围 |
说明 |
| X1 |
entrypoint.sh 与 entrypoint-test.sh(归档) 高度重复 |
2 个文件(其中1个已归档) |
开发工具检测(12行)、dev-tools.sh 写入(8行)、cleanup函数(8行)、trap注册(1行)、SSH启动+检查(10行)、ttyd启动+检查(8行)、信息展示(18行)、exec sleep(1行) —— 约 66 行完全相同,占总代码量 66/210 = 31%(不计公共部分则重复率更高)。优先级大幅降低(entrypoint-test.sh 已归档)。 |
| X2 |
三个版本的 cleanup 函数逐字相同 |
3 个文件 |
Ubuntu/Alpine/Test 三份 entrypoint 的 cleanup 函数(第7-14行)完全一致。这是典型的 DRY 违反。 |
| X3 |
ttyd 认证密码三套不同策略 |
3 个文件 |
Ubuntu版无密码 / Alpine版 ada:123 / Test版 jc:1234567。安全级别不一致,容易在部署时混淆。[已修复] — 当前版本统一从 ${TTYD_CREDENTIALS} 环境变量读取。 |
| X4 |
dev-tools.sh 内容在 Alpine 和 Test 版中逐字相同 |
2 个文件 |
entrypoint.sh 第80-88行与 entrypoint-test.sh 第32-40行的 heredoc 内容完全一致。 |
| X5 |
SSHD_PID 跟踪在所有版本中都无效 |
3 个文件(其中1个已归档) |
三个版本的 entrypoint 都声明了 SSHD_PID=0 且从未赋值,cleanup 中的 kill "$SSHD_PID" 分支永远不会执行。这是一个系统性缺陷。(低优先级,未修复) |
| X6 |
端口号/标题/密码散落在各脚本中 |
3 个文件(其中1个已归档) |
修改端口或密码需要在多个文件中同步修改,遗漏任一文件会导致配置不一致。[已改善] — 当前版本已集中到 docker-compose.yml 的 environment 配置中管理。 |
Top 10 改进建议
| 优先级 |
建议 |
涉及脚本 |
收益 |
| P0 |
重构 entrypoint 公共框架(配置驱动) |
entrypoint.sh (x2, 其中1个已归档) |
消除 ~66 行重复代码,新增容器变体只需配置文件(优先级降低:entrypoint-test.sh 已归档) |
| P1 |
修复 SSHD_PID 追踪(统一方案) |
entrypoint.sh (x3) |
实现 sshd 优雅关闭,防止孤儿进程 |
| P2 |
ttyd-session.sh 添加 set -uo pipefail + 输入校验 |
ttyd-session.sh |
防止空 session 名和越界访问 |
| P3 |
统一 ttyd 认证策略,密码从环境变量读取 |
entrypoint.sh (x3) |
消除安全配置不一致,支持灵活部署(已完成) |
| P4 |
ttyd-session.sh 清理 session 名尾部冒号 |
ttyd-session.sh |
UI 显示更干净,避免意外行为 |
| P5 |
entrypoint-test.sh sudoers 文件写入增加原子性 |
entrypoint-test.sh |
防止中断导致 sudo 不可用 |
| P6 |
document 全权限安全策略的影响范围 |
entrypoint-test.sh |
让使用者明确了解 allow:["*"] + NOPASSWD:ALL 的安全含义 |
| P7 |
端口/标题/密码等配置外部化为环境变量 |
entrypoint.sh (x3) |
docker-compose.yml 集中管理,消除散落配置 |
| P8 |
考虑 entrypoint.sh shebang 改为 /bin/sh |
entrypoint.sh (Alpine x2) |
减少 bash 依赖,Alpine 环境更轻量(需确认无 bashism) |
| P9 |
sshd 启动增加 -D 模式或 PID 文件追踪 |
entrypoint.sh (Alpine x2) |
更精确的进程生命周期管理 |
附录:历史 Bug 回顾
| Bug |
影响 |
当前状态 |
| grep -P 在 BusyBox 不可用 |
Alpine 环境 TTYD_QUERY_STRING 解析失败 |
ttyd-session.sh 第9行已改用 tr & '\n' | grep '^session=' | cut -d= -f2 管道链,完全兼容 BusyBox |
| heredoc 单引号阻止变量展开 |
.bashrc 中 ANTHROPIC_AUTH_TOKEN 等环境变量无法传递到 developer 用户 |
entrypoint-test.sh 第64行已改用 << BASHRC 双引号 heredoc,变量在写入时正确展开;\$PATH 转义确保字面量输出 |
| dev-tools.sh 反斜杠转义问题 |
profile.d 脚本中 \$PATH 或 \\ 导致语法错误 |
当前版本 dev-tools.sh 使用单引号 heredoc << 'PROFILE',内容原样写入无需转义,已修复 |