<!-- title: 造镜像瘦身安全审查Skill -->

> 本教程是「Claude Code 深度使用」系列第 2 轮，聚焦 **Skills 开发实战**——从零造一个自定义技能。你将动手打造一个 **Dockerfile 双体检技能**（`dockerfile-audit`），它同时做两件事：扫描安全隐患（root 运行、密钥入层、`COPY .` 全量拷贝等）和发现镜像瘦身机会（缺多阶段、肥基础镜像、apt 残留缓存）。学完你能掌握技能的目录结构、frontmatter 配置、确定性脚本 + 动态注入 + 模型上下文判断三层混合写法，以及用官方 `skill-creator` 跑评测闭环验证质量。

---

## 一、适用人群与前置条件

**适用人群**：已会用 Claude Code 基本对话、想把自己的重复工作流沉淀成「一行命令技能」的开发者；负责容器安全 / 平台工程的工程师。

**前置条件**：

- 已安装 Claude Code v2.1.196 及以上（下文用到的 `${CLAUDE_PROJECT_DIR}`、`skill-creator` 评测、`skillOverrides` 等都依赖近版本）。
- 本机有 `python3`（脚本只用标准库，无需 pip 安装）。
- 一个含 `Dockerfile` 的练习项目，或跟着本文造一个故意写坏的样例。

**为什么是「双体检」**：纯模型审查容易凭印象下结论、可重复性差；纯规则脚本死板、看不出上下文。本教程的核心设计是 **「确定性脚本给骨架，模型补血肉」**——脚本跑规则出结构化发现，模型再结合真实 Dockerfile 补充脚本判断不了的点，这是生产级 Skill 最值得掌握的混合范式。

---

## 二、先理解 Skill 的本质

Claude Code 的 Skill 遵循开放的 **Agent Skills 标准**，本质就是「一个目录 + 一个 `SKILL.md` 入口」。它与 `CLAUDE.md` 最大的区别在于 **惰性加载**：`CLAUDE.md` 每次会话都进上下文，而技能正文只有在被触发（你敲 `/name`，或 Claude 判定相关）时才进入上下文。因此长篇参考资料塞进技能几乎不占成本，直到真正用到。

技能可放在三处，决定谁能用：

| 位置 | 路径 | 作用范围 |
|:--|:--|:--|
| 个人级 | `~/.claude/skills/<name>/SKILL.md` | 你所有项目 |
| 项目级 | `.claude/skills/<name>/SKILL.md` | 仅本项目（可提交到版本库共享给团队） |
| 企业级 | 受管设置下发 | 全组织 |

本教程把 `dockerfile-audit` 放在**个人级**，这样你在任意含 Dockerfile 的仓库里都能 `/dockerfile-audit`。

---

## 三、动手：六步造出 dockerfile-audit 技能

### 第 1 步：规划目录结构

```text
dockerfile-audit/
├── SKILL.md            # 入口：frontmatter + 指令（必需）
├── checks.md           # 规则细则参考（按需加载，不进主上下文）
└── scripts/
    └── audit.py        # 确定性扫描脚本（被执行，不被加载进上下文）
```

要点：`SKILL.md` 控制在 500 行以内，长细则挪到 `checks.md`，由主文件按需引用。技能目录建立后会被 Claude Code **实时监听**，同会话内增删改即生效，无需重启。

创建骨架：

```bash
mkdir -p ~/.claude/skills/dockerfile-audit/scripts
```

### 第 2 步：写 SKILL.md 的 frontmatter

frontmatter 是技能的「说明书」，决定它**何时被触发**、**谁来触发**、**免审批哪些工具**。

```yaml
---
name: dockerfile-audit
description: 审查 Dockerfile 的安全隐患与镜像瘦身机会。当用户提到 Dockerfile 审查、镜像安全、容器加固、镜像瘦身、减小镜像体积时使用。输出按严重级别排序、带行号与修复命令的清单。
argument-hint: [Dockerfile路径，默认 ./Dockerfile]
disable-model-invocation: true
allowed-tools: Bash(python3 *)
---
```

逐字段说明（这是写好技能的关键）：

- **`description`**：写清**做什么**+**何时用**，把最高频触发词放最前。注意它和 `when_to_use` 合并在技能列表里被**截断到 1536 字符**，所以「先放重点」。Claude 是否自动触发技能，几乎全看这段文字。
- **`disable-model-invocation: true`**：本技能是**有意触发的审查动作**，禁止 Claude 自作主张去跑。这样它只在你敲 `/dockerfile-audit` 时执行——审查类、部署类、有副作用的工作流都建议这么配。
- **`allowed-tools: Bash(python3 *)`**：技能激活期间，Claude 跑 `python3` 不再逐次问你。注意它**只是免审批，不是限制**，其它工具仍受你全局权限规则约束。
- **`argument-hint`**：补全时给提示，让使用者知道可以传一个 Dockerfile 路径。

> ⚠️ 安全提醒：项目级技能的 `allowed-tools` 在你**信任工作区**后才生效。克隆陌生仓库时先看一眼它的 `.claude/skills/`，技能能给自己授予很宽的工具权限。

### 第 3 步：写确定性扫描脚本 `scripts/audit.py`

脚本只做**纯规则匹配**，输出 JSON，保证每次跑同一文件结果一致：

```python
#!/usr/bin/env python3
"""Dockerfile 静态扫描：安全 + 瘦身。仅依赖标准库。"""
import json, re, sys
from pathlib import Path

SEV = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
SECRET = re.compile(r"(?i)(password|passwd|secret|token|api[_-]?key|private[_-]?key|aws_access)")

def audit(path):
    lines = Path(path).read_text(errors="replace").splitlines()
    findings, has_user, has_hc, stages, bases = [], False, False, 0, []
    for i, raw in enumerate(lines, 1):
        s, up = raw.strip(), raw.strip().upper()
        m = re.match(r"FROM\s+(\S+)", s, re.I)
        if m:                                          # FROM 行：版本锁定 + 基础镜像体积
            img = m.group(1); stages += 1; bases.append(img)
            if ":latest" in img or ("@" not in img and ":" not in img.split("@")[0]):
                findings.append({"line": i, "sev": "HIGH", "cat": "security", "rule": "unpinned-base",
                    "msg": f"基础镜像未锁版本/用 :latest ({img})，应固定到 digest 或明确 tag"})
            if re.search(r"(ubuntu|debian|centos|fedora)\b", img, re.I) and "slim" not in img:
                findings.append({"line": i, "sev": "MEDIUM", "cat": "size", "rule": "heavy-base",
                    "msg": f"完整 OS 镜像 ({img})，可换 alpine/distroless/slim"})
        if up.startswith("USER "): has_user = True
        if up.startswith("HEALTHCHECK"): has_hc = True
        if re.match(r"ADD\s+https?://", s, re.I):
            findings.append({"line": i, "sev": "LOW", "cat": "security", "rule": "add-url",
                "msg": "ADD 远程 URL 不可复现且有供应链风险，改 COPY + 多阶段"})
        if re.match(r"COPY\s+\.\s+", s, re.I):
            findings.append({"line": i, "sev": "HIGH", "cat": "security", "rule": "copy-dot-all",
                "msg": "COPY . 会把 .git/.env/密钥一起带进镜像，配 .dockerignore 并精确指定路径"})
        if (up.startswith("ENV ") or up.startswith("ARG ")) and SECRET.search(s):
            findings.append({"line": i, "sev": "CRITICAL", "cat": "security", "rule": "secret-in-layer",
                "msg": "ENV/ARG 疑似硬编码密钥，构建层会永久留存，改运行期注入"})
        if "apt-get install" in s and "--no-install-recommends" not in s:
            findings.append({"line": i, "sev": "LOW", "cat": "size", "rule": "apt-recommends",
                "msg": "未加 --no-install-recommends，装入推荐包"})
        if "apt-get install" in s and not any("rm -rf /var/lib/apt" in l for l in lines[i:i+5]):
            findings.append({"line": i, "sev": "MEDIUM", "cat": "size", "rule": "apt-no-clean",
                "msg": "apt 安装后未同层清理 /var/lib/apt/lists，缓存烤进镜像"})
        if re.match(r"apt-get\s+(upgrade|dist-upgrade)", s, re.I):
            findings.append({"line": i, "sev": "MEDIUM", "cat": "size", "rule": "apt-upgrade",
                "msg": "apt-get upgrade 让层不可复现且增大体积，交给基础镜像维护"})
    if not has_user:
        findings.append({"line": 0, "sev": "CRITICAL", "cat": "security", "rule": "runs-as-root",
            "msg": "默认以 root 运行，增加 USER 指令指定非 root 用户"})
    if not has_hc:
        findings.append({"line": 0, "sev": "LOW", "cat": "security", "rule": "no-healthcheck",
            "msg": "缺少 HEALTHCHECK，编排器无法感知真实健康"})
    if stages <= 1:
        findings.append({"line": 0, "sev": "MEDIUM", "cat": "size", "rule": "no-multistage",
            "msg": "未多阶段构建，编译器/构建工具残留进最终镜像"})
    findings.sort(key=lambda f: (SEV[f["sev"]], f["line"]))
    return findings

if __name__ == "__main__":
    p = sys.argv[1] if len(sys.argv) > 1 else "Dockerfile"
    print(json.dumps({"file": p, "findings": audit(p)}, ensure_ascii=False, indent=2))
```

**预期结果**：对一个写坏的 Dockerfile，会吐出按 `CRITICAL→HIGH→MEDIUM→LOW` 排好序的 JSON 数组，每条带 `line / sev / cat / rule / msg`。

**常见问题**：
- 脚本报 `FileNotFoundError`：传入的 Dockerfile 路径不对，确认相对/绝对路径。
- 输出太多噪音：调高阈值——把 `LOW` 过滤掉，或细化正则（如 `apt-no-clean` 的前瞻窗口从 5 行扩到 8 行）。

### 第 4 步：写 SKILL.md 正文（混合三层）

这一步是技能质量的核心。正文要做三件事：**动态注入目标文件** → **跑脚本拿骨架** → **让模型补血肉**。

````markdown
# Dockerfile 安全 + 瘦身双体检

对给定 Dockerfile 做静态扫描并给出可执行修复建议。审查以确定性脚本结果为准，再补充脚本判断不了的上下文点。

## 目标文件
!`cat $ARGUMENTS 2>/dev/null || cat Dockerfile`

## 第一步：跑确定性扫描
执行内置脚本，得到结构化发现（JSON）：
```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/audit.py "${CLAUDE_PROJECT_DIR}/$ARGUMENTS"
```

## 第二步：补充上下文判断
脚本只做规则匹配。请结合上面读到的真实 Dockerfile 内容，补充脚本无法判断的问题，至少检查：
- WORKDIR 是否是写死的绝对路径、是否配合 chown 避免权限坑；
- ENTRYPOINT/CMD 是否暴露了调试端口或 shell 注入面（如 `sh -c` 直接拼变量）；
- 多个 RUN 是否能合并成一层以减少镜像层数；
- .dockerignore 是否配套（如脚本里命中 copy-dot-all，务必点名提醒）。

## 输出格式
严格按下表结构输出，按严重级别排序，每条含：级别、类别(安全/瘦身)、所在行、规则名、问题、**可直接执行的修复命令或片段**。最后给一段「优先修前三项」的总结，并估算瘦身收益（如「换 alpine + 多阶段预计 -180MB」）。

完整规则细则见 [checks.md](checks.md)。
````

三个语法点务必吃透：

1. **`` !`cat Dockerfile` `` 动态注入**：在技能正文送给模型**之前**，Claude Code 先执行该命令，把输出原地替换进文本。模型看到的是**真实文件内容**而非命令本身——这让审查「接地气」。它只替换一次，且要求 `!` 出现在行首或紧跟空白。
2. **`${CLAUDE_SKILL_DIR}`**：指向技能自己的目录，使 `audit.py` 无论装在个人级、项目级还是插件里都能被正确引用。
3. **`$ARGUMENTS`**：你敲 `/dockerfile-audit api/Dockerfile` 时，`api/Dockerfile` 就替换进来。

### 第 5 步：补一份 checks.md（按需加载的细则）

把每条规则背后的「为什么、修成什么样」写成样例，挂在 `checks.md`，由 `SKILL.md` 引用。这样**主上下文只留骨架**，模型只在需要时按需读细则——这就是官方强调的「渐进式披露（progressive disclosure）」。片段示例：

```markdown
## runs-as-root (CRITICAL, security)
**为何危险**：容器以 uid 0 运行，逃逸即获宿主 root。
**修复**：
    RUN groupadd -r app && useradd -r -g app app
    USER app
```

### 第 6 步：安装、触发、免审批

保存文件后无需重启（实时监听生效）。进入任意含 Dockerfile 的项目，启动 `claude`，直接：

```text
/dockerfile-audit
```

或显式指定文件：

```text
/dockerfile-audit services/api/Dockerfile
```

因为设了 `allowed-tools: Bash(python3 *)`，跑 `audit.py` 时不会再弹权限确认。审查结果会按级别排序、带行号、带修复命令地输出。

**常见问题**：
- **`/dockerfile-audit` 不出现**：frontmatter YAML 写错会以「空元数据」加载，`/名字` 仍能跑但 Claude 拿不到 `description` 去自动匹配。用 `claude --debug` 启动可看到解析报错。
- **技能触发太频繁**（若你关掉 `disable-model-invocation`）：把 `description` 写更窄，或干脆保留 `disable-model-invocation: true` 只手动触发。
- **技能列表太长，描述被截断**：跑 `/doctor` 看哪些描述被砍。低优先级的可在 `skillOverrides` 设为 `"name-only"` 给别的技能腾预算。

---

## 四、实战案例：给一个「百病缠身」的 Dockerfile 体检

把下面这份故意写坏的文件存为 `./Dockerfile`：

```dockerfile
FROM ubuntu:latest
ENV DB_PASSWORD=s3cret
RUN apt-get update && apt-get install -y python3 pip curl vim
COPY . /app
RUN pip install -r /app/requirements.txt
CMD ["python3", "/app/main.py"]
```

执行 `/dockerfile-audit`，你将得到类似下面的报告（脚本 + 模型合议）：

| 级别 | 类别 | 行 | 规则 | 问题 | 修复 |
|:--|:--|:--|:--|:--|:--|
| CRITICAL | 安全 | 2 | secret-in-layer | `DB_PASSWORD` 烤进层 | 改运行期注入 `-e`/secret |
| CRITICAL | 安全 | — | runs-as-root | 默认 root | 增加 `USER app` |
| HIGH | 安全 | 1 | unpinned-base | `ubuntu:latest` | 锁 `python:3.12-slim` |
| HIGH | 安全 | 4 | copy-dot-all | `COPY .` 全量拷贝 | 配 `.dockerignore`，精确 COPY |
| MEDIUM | 瘦身 | 3 | apt-no-clean | 未清缓存 | 同层 `rm -rf /var/lib/apt/lists/*` |
| MEDIUM | 瘦身 | 3 | apt-recommends | 装入推荐包 | 加 `--no-install-recommends` |
| MEDIUM | 瘦身 | — | no-multistage | 无多阶段 | 拆 builder/运行两阶段 |
| LOW | 瘦身 | — | no-healthcheck | 缺探活 | 加 `HEALTHCHECK CMD ...` |

**结论**：优先修前三项（密钥入层、root 运行、`COPY .`）；改用 `python:3.12-slim` + 多阶段 + 清缓存，预计镜像从约 420MB 降到 130MB。

---

## 五、整体工作流程图

```mermaid
flowchart TD
    A["你敲 /dockerfile-audit api/Dockerfile"] --> B["Claude Code 渲染 SKILL.md"]
    B --> C{"动态注入 !`cat Dockerfile`"}
    C -->|替换为真实文件内容| D["$ARGUMENTS → api/Dockerfile"]
    B --> E["模型读正文指令"]
    E --> F["跑 audit.py<br/>确定性规则匹配"]
    F --> G["输出 JSON 发现"]
    G --> H["模型结合真实文件<br/>补充上下文判断"]
    H --> I["按级别排序输出<br/>带行号+修复命令"]
    C -.失败/默认.-> J["cat Dockerfile"]
    style C fill:#fef3c7
    style F fill:#dbeafe
    style H fill:#dcfce7
```

---

## 六、用 skill-creator 跑评测闭环（进阶）

「技能被触发」只说明 Claude 找到了它，**不等于**它做对了。要量化质量，用官方 `skill-creator` 插件做基线对比：

```text
/plugin install skill-creator@claude-plugins-official
/reload-plugins
```

随后让它评测你的技能：

```text
请用 skill-creator 评测我的 dockerfile-audit 技能
```

它会：准备一批测试 prompt（含坏/好 Dockerfile 与期望断言）存到技能目录 `evals/evals.json`；每个 case 起一个**干净上下文子代理**分别跑「有技能 / 无技能」；按断言判分写 `grading.json`；最后汇总成 `benchmark.json`，给出**启用技能后通过率提升了多少**，以及相应的 token/耗时开销。改一版 frontmatter 或脚本就重跑一次 A/B，确认「是真改进」再提交——这是把技能从「能用」做到「好用」的标准动作。

---

## 七、延伸阅读

- **官方 Skills 总览**：`code.claude.com/docs/en/skills`——frontmatter 全字段、子代理执行、动态注入的权威说明。
- **Agent Skills 开放标准**：`agentskills.io`——跨工具通用的技能规范与评测方法。
- **skill-creator 插件**：`github.com/anthropics/claude-plugins-official`——评测/基准/版本对比/描述调优的官方工具。
- **技能 vs 子代理 vs CLAUDE.md 选型**：在「同一件事该用哪种扩展机制」上做决策时参考官方 Subagents / Memory 文档。

---

## 八、训练营适用性评估

- **适合的营型**：企业「容器安全 / 平台工程」专题营、DevSecOps 实战营、AI 编程效率提升营。技能产出物可立即并入团队 CI（把 `audit.py` 接进 pre-commit 或流水线）。
- **教学时长**：讲师带做约 **90 分钟**（讲透 frontmatter + 混合三层写法 + 评测闭环）；学员独立完成一个新技能（如改造成「compose 文件加固」「k8s manifest 审查」）再约 **60 分钟**。
- **进阶作业建议**：① 给 `audit.py` 增加 `--format sarif` 输出，对接 GitHub Code Scanning；② 把 `disable-model-invocation` 关掉，对比自动触发命中率，用 skill-creator 调 `description`；③ 把技能提交到项目级 `.claude/skills/` 并写 `.dockerignore` 自动生成子技能。

---

## 参考来源

1. [Extend Claude with skills（官方 Skills 文档）](https://code.claude.com/docs/en/skills) — 技能目录结构、frontmatter 全字段、动态注入、子代理执行、评测与权限的权威说明。
2. [Creating custom skills（Anthropic 官方指南）](https://claude.com/docs/skills/how-to) — 如何从零创建、组织、测试自定义技能。
3. [Agent Skills 开放标准](https://agentskills.io) — 跨工具通用的技能规范与「评测技能输出质量」的迭代工作流。
4. [Improving skill-creator（官方博客）](https://claude.com/blog/improving-skill-creator-test-measure-and-refine-agent-skills) — skill-creator 的基准/版本对比/描述调优机制详解。
5. [How to Create a SKILL.md File from Scratch (2026)](https://www.agensi.io/learn/create-skill-md-from-scratch) — 从零编写 SKILL.md 的社区实战教程，补充 frontmatter 写法范例。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
