用 Claude Code 做“可检查”的维护脚本:从 dry-run 开始的安全自动化教程
- 原文:<https://www.xda-developers.com/claude-code-helped-maintenance-scripts-inspect/>
- 原文标题:Claude Code helped me build maintenance scripts I could actually inspect, not just run
- 作者 / 来源:Jeff Butts / XDA Developers
- 发布时间:2026-06-02
- 已保存摘要:
/home/lin/.hermes/projects/hermes-gsummary-workflow/runs/outputs/20260603-074607-Claude-Code-helped-me-build-maintenance-scripts-I-could-actually-inspect,-not-ju-2307719-243669760-summary.md - 本文定位:原文事实 + 本文提炼 + 实践扩展。示例脚本是为了教学补充,不是原文提供的完整项目。
1. 先给结论
如果你想用 Claude Code、Codex 或其他 AI coding 工具写运维维护脚本,不要从“帮我自动清理服务器”开始。
更安全的起点是:先让 AI 写一个只读检查脚本,默认 dry-run,输出它准备观察或触碰的对象;等你能看懂、能复现、能验证,再考虑真实执行。
这篇教程把原文的 home lab 经验改造成一个可复制的运维脚本工作流:
- 把维护任务拆成小脚本;
- 每个脚本只做一件事;
- 先实现 dry-run / 只读检查;
- 明确路径边界、排除目录、日志和退出码;
- 用真实样例验证输出,再让人决定是否升级为执行脚本。
2. 原文事实:作者真正强调的不是“AI 更快”
原文讲的是作者在 home lab 中用 Claude Code 写维护脚本,包括:
- 检查备份目录;
- 汇总 Docker 容器状态;
- 标记过大的目录;
- 清理旧项目文件夹;
- 给危险操作加入 dry-run、路径校验、日志和确认。
作者的核心观点是:Claude Code 的价值不是让脚本变成不可见的自动化黑盒,而是让脚本更容易被人检查、解释和修改。
原文也明确提醒:AI 生成脚本可能看起来很完整,但仍会误判路径、服务名、文件夹含义或边界条件。像 old-but-important 这种目录,AI 不知道它的重要性,除非你把规则写清楚。
3. 本文提炼:安全维护脚本的最小工作流
可以把 AI 辅助维护脚本拆成 5 个阶段:
- 观察阶段:只读检查,不修改文件、不重启服务、不删除目录。
- 解释阶段:脚本输出检查项、命中规则、风险和退出码。
- dry-run 阶段:如果将来要移动、删除、归档,也先只打印计划动作。
- 人工确认阶段:人审查输出,确认范围正确。
- 执行阶段:只有低风险、可回滚、路径明确的动作,才允许进入真实执行。
第一版不要接数据库、Kubernetes、生产服务,也不要给 AI 一个通用 shell 工具。先从本地样例和只读脚本开始。
4. 给 Claude Code 的提示词模板
把模糊请求改成有边界的请求。
不推荐
帮我写一个脚本清理旧文件和检查服务。推荐
请帮我写一个 Python 维护检查脚本,要求:
目标:检查 home lab 目录状态,只读输出报告,默认不做任何修改。
边界:
- 只允许读取我传入的 --root 目录;
- 不允许删除、移动、重启服务或修改配置;
- 如果未来需要归档旧目录,也必须先实现 --dry-run,只打印计划动作;
- 排除目录 old-but-important;
- 所有函数必须有类型注解;
- 不使用 shell=True;
- 不引入第三方依赖。
检查项:
1. backups 目录是否存在;
2. logs/*.log 是否超过 1MB;
3. projects 下超过 30 天未修改的目录是否需要归档;
4. 输出清晰日志;
5. 有明确退出码:0=正常,1=参数或路径错误,2=发现告警。
请先输出代码,然后解释每个安全边界。这类提示词的重点不是“写得快”,而是把执行权关在笼子里。
5. 可运行示例:只读维护检查脚本
下面示例只使用 Python 标准库。它会检查备份目录、大日志文件和陈旧项目目录。即使发现旧目录,也只打印 would archive,不会真的移动或删除。
创建文件:maintenance_check.py
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class CheckResult:
name: str
status: str
detail: str
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Safe home-lab maintenance checker")
parser.add_argument("--root", required=True, help="Allowed root directory")
parser.add_argument("--dry-run", action="store_true", help="Show planned actions only")
parser.add_argument("--max-log-mb", type=int, default=1, help="Warn when log files exceed this size")
parser.add_argument("--stale-days", type=int, default=30, help="Warn when project folders are older than this")
return parser.parse_args()
def resolve_root(raw_root: str) -> Path:
root = Path(raw_root).expanduser().resolve()
if not root.exists() or not root.is_dir():
raise ValueError(f"root is not a directory: {root}")
return root
def ensure_inside_root(root: Path, candidate: Path) -> Path:
resolved = candidate.resolve()
if root not in [resolved, *resolved.parents]:
raise ValueError(f"path escapes allowed root: {resolved}")
return resolved
def check_backup_dir(root: Path) -> CheckResult:
backup_dir = ensure_inside_root(root, root / "backups")
if backup_dir.exists():
return CheckResult("backup_dir", "OK", f"found {backup_dir}")
return CheckResult("backup_dir", "WARN", f"missing {backup_dir}")
def check_large_logs(root: Path, max_log_mb: int) -> list[CheckResult]:
log_dir = ensure_inside_root(root, root / "logs")
if not log_dir.exists():
return [CheckResult("large_logs", "WARN", f"missing {log_dir}")]
limit_bytes = max_log_mb * 1024 * 1024
results: list[CheckResult] = []
for path in sorted(log_dir.glob("*.log")):
size = path.stat().st_size
if size > limit_bytes:
results.append(CheckResult("large_logs", "WARN", f"{path.name} is {size} bytes"))
if not results:
results.append(CheckResult("large_logs", "OK", "no oversized log files"))
return results
def check_stale_projects(root: Path, stale_days: int) -> list[CheckResult]:
projects_dir = ensure_inside_root(root, root / "projects")
if not projects_dir.exists():
return [CheckResult("stale_projects", "WARN", f"missing {projects_dir}")]
now = Path.cwd().stat().st_mtime
threshold_seconds = stale_days * 24 * 60 * 60
results: list[CheckResult] = []
for path in sorted(projects_dir.iterdir()):
if not path.is_dir():
continue
if path.name in {"old-but-important"}:
results.append(CheckResult("stale_projects", "SKIP", f"excluded {path.name}"))
continue
age_seconds = now - path.stat().st_mtime
if age_seconds > threshold_seconds:
planned = f"would archive {path} to {root / 'archive' / path.name}"
results.append(CheckResult("stale_projects", "DRY-RUN", planned))
if not results:
results.append(CheckResult("stale_projects", "OK", "no stale project folders"))
return results
def print_results(results: list[CheckResult]) -> None:
for item in results:
print(f"[{item.status}] {item.name}: {item.detail}")
def main() -> int:
args = parse_args()
try:
root = resolve_root(args.root)
results = [check_backup_dir(root)]
results.extend(check_large_logs(root, args.max_log_mb))
results.extend(check_stale_projects(root, args.stale_days))
print_results(results)
if any(item.status == "WARN" for item in results):
return 2
return 0
except ValueError as exc:
print(f"[ERROR] validation: {exc}")
return 1
if __name__ == "__main__":
raise SystemExit(main())6. 准备样例目录
mkdir -p /tmp/maint-ai-demo/sample_home_lab/{backups,projects/old-but-important,projects/tmp-old,logs,compose}
printf 'important\n' > /tmp/maint-ai-demo/sample_home_lab/projects/old-but-important/README.txt
printf 'scratch\n' > /tmp/maint-ai-demo/sample_home_lab/projects/tmp-old/notes.txt
printf 'version: "3"\nservices:\n demo:\n image: nginx:alpine\n' > /tmp/maint-ai-demo/sample_home_lab/compose/docker-compose.yml
truncate -s 2M /tmp/maint-ai-demo/sample_home_lab/logs/app.log
touch -d '45 days ago' /tmp/maint-ai-demo/sample_home_lab/projects/tmp-old /tmp/maint-ai-demo/sample_home_lab/projects/tmp-old/notes.txt7. 场景一:每日主机健康检查
运行:
python3 maintenance_check.py \
--root /tmp/maint-ai-demo/sample_home_lab \
--dry-run \
--max-log-mb 1 \
--stale-days 30实际验证输出节选:
[OK] backup_dir: found /tmp/maint-ai-demo/sample_home_lab/backups
[WARN] large_logs: app.log is 2097152 bytes
[SKIP] stale_projects: excluded old-but-important
[DRY-RUN] stale_projects: would archive /tmp/maint-ai-demo/sample_home_lab/projects/tmp-old to /tmp/maint-ai-demo/sample_home_lab/archive/tmp-old
exit=2验收标准:
backups存在时输出OK;- 超过阈值的日志输出
WARN; old-but-important被明确跳过;- 旧项目只输出
would archive,不真实移动; - 有告警时退出码为
2。
8. 场景二:旧项目归档前的 dry-run 审查
这个脚本故意没有实现真实归档。原因很简单:归档、移动、删除都属于写操作,第一版只应该证明“识别逻辑正确”。
审查重点:
- 它是否只扫描
--root/projects; - 排除目录是否生效;
- 计划目标是否在
--root/archive下; - 输出是否足够让人判断“该不该执行”。
如果你后续要实现真实归档,应该另开一个函数,例如 archive_project(source: Path, target: Path, dry_run: bool) -> CheckResult,并继续保留 dry-run 默认路径。不要把检查逻辑和执行逻辑混在一起。
9. 场景三:错误输入必须显式失败
运行一个不存在的目录:
python3 maintenance_check.py --root /tmp/maint-ai-demo/missing --dry-run实际验证输出:
[ERROR] validation: root is not a directory: /tmp/maint-ai-demo/missing
exit=1验收标准:
- 路径错误不能被模型或脚本“温柔地总结成正常”;
- 必须输出错误原因;
- 必须用非 0 退出码告诉上层调度器失败。
10. 让 AI 解释脚本,而不是替你信任脚本
生成脚本后,继续让 Claude Code 做审查型任务,而不是马上让它扩展功能。
可复制提示词:
请审查这个维护脚本,只关注安全边界,不新增功能。
检查项:
1. 是否有路径逃逸风险;
2. 是否存在 shell=True 或隐式 shell 执行;
3. 是否有真实删除、移动、重启服务、写配置动作;
4. dry-run 是否只是展示计划,不产生副作用;
5. 是否能通过退出码区分正常、告警和参数错误;
6. 哪些地方必须人工确认后才能进入执行模式。
请按 blocking / important / optional 分类输出。这一步的价值是让 AI 帮你暴露风险,而不是让 AI 替你承担风险。
11. 升级到真实环境前的安全清单
上线前至少检查:
- 默认模式只读;
- 写操作必须显式加参数,例如
--apply; --dry-run是默认或强制先执行;- 所有路径都经过 allowlist 校验;
- 排除目录写在配置或常量里,不能靠记忆;
- 不使用
shell=True; - 不读取
.env、私钥、token、数据库配置等敏感文件; - 日志包含“准备做什么”,不是只输出“完成”;
- 失败不能被吞掉;
- 每次真实执行前保留 dry-run 输出。
12. 一周落地计划
- 第 1 天:列出 3 个只读维护任务,例如备份目录检查、日志大小检查、服务状态检查。
- 第 2 天:让 AI 生成只读脚本,禁止任何写操作。
- 第 3 天:准备样例目录和失败样例,跑出真实输出。
- 第 4 天:让 AI 做安全审查,只修 blocking 问题。
- 第 5 天:接入定时任务,但只发送报告,不自动修复。
- 第 6 天:把最常见的告警分类,减少噪声。
- 第 7 天:评估是否需要某个低风险动作进入
--apply,并先设计回滚和人工确认。
13. 常见坑
- 直接让 AI 写“清理脚本”,但没有排除目录;
- 没有 dry-run,测试等于真实执行;
- 输出只有“done”,没有说明处理了哪些路径;
- 把服务重启、文件删除、配置修改混在一个脚本里;
- 让模型总结命令输出,却没有检查命令是否失败;
- 把 home lab 经验直接套到生产环境。
14. 最小原则
维护脚本的第一目标不是“自动化一切”,而是:
让重复检查更容易发生,让危险动作更容易被看见。
这正是原文最值得复用的地方:Claude Code 可以帮你更快写脚本,但你应该用它来放大可读性、可验证性和边界感,而不是放大盲目信任。