关于 health
health 是一项技能,用于审计项目 Claude Code 配置的六个层面,涵盖 CLAUDE.md、规则、技能、钩子、子 Agent 和验证器。它会按项目复杂度调整检查,并报告问题而不直接修改配置。
主要功能
- 01六层配置审计
- 02按项目级别调整检查
- 03安全与行为分析
- 04结构化问题报告
- 05不自动修改配置
适用场景
- 01检查项目的 Claude Code 配置
- 02按项目复杂度发现配置缺口
- 03评估技能、钩子和 MCP 设置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ hiclaude/ health --skill health安装后在 Claude Code 中输入/health确认已加载。
附带文件agents/agent1-context.mdagents/agent2-control.md
详细介绍
Claude Code 配置健康审计
使用六层框架审计当前项目的 Claude Code 设置:
CLAUDE.md → rules → skills → hooks → subagents → verifiers
目标是找出违规项并识别未对齐的层级,同时根据项目复杂度调整审计尺度。
输出语言: 按以下顺序检查:(1) CLAUDE.md 中的 ## Communication 规则(全局优先于本地);(2) 用户最近对话消息的语言;(3) 默认使用英语。将检测到的语言用于所有输出,包括进度行、报告和停止条件问题。
重要: 首次调用工具前,先使用输出语言输出进度块:
Step 1/3: Collecting configuration data
· CLAUDE.md (global + local) · rules/ · settings.local.json · hooks
· MCP servers · skills inventory + security scan
· conversation history (up to 3 recent sessions)
步骤 0:评估项目级别
选择级别:
| 级别 | 信号 | 预期配置 |
|---|---|---|
| 简单 | <500 个项目文件,1 位贡献者,无 CI | 仅需 CLAUDE.md;0–1 个 skills;无需 rules/;hooks 可选 |
| 标准 | 500–5K 个项目文件,小型团队或已启用 CI | CLAUDE.md + 1–2 个 rules 文件;2–4 个 skills;基础 hooks |
| 复杂 | >5K 个项目文件,多位贡献者、多种语言、CI 活跃 | 必须具备完整的六层配置 |
仅应用检测到的级别所要求的配置。
步骤 1:收集所有数据(单个 bash 代码块)
运行一个代码块来收集数据。
P=$(pwd)
SETTINGS="$P/.claude/settings.local.json"
echo "=== TIER METRICS ==="
echo "project_files: $(git -C "$P" ls-files 2>/dev/null | wc -l || find "$P" -type f -not -path "*/.git/*" -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*" | wc -l)"
echo "contributors: $(git -C "$P" log -n 500 --format='%ae' 2>/dev/null | sort -u | wc -l)"
echo "ci_workflows: $(ls "$P/.github/workflows/"*.yml "$P/.github/workflows/"*.yaml 2>/dev/null | wc -l)"
echo "skills: $(find "$P/.claude/skills" -name "SKILL.md" 2>/dev/null | grep -v '/health/SKILL.md' | wc -l)"
echo "claude_md_lines: $(wc -l < "$P/CLAUDE.md" 2>/dev/null)"
echo "=== CLAUDE.md (global) ===" ; cat ~/.claude/CLAUDE.md 2>/dev/null || echo "(none)"
echo "=== CLAUDE.md (local) ===" ; cat "$P/CLAUDE.md" 2>/dev/null || echo "(none)"
echo "=== settings.local.json ===" ; cat "$SETTINGS" 2>/dev/null || echo "(none)"
echo "=== rules/ ===" ; find "$P/.claude/rules" -name "*.md" 2>/dev/null | while IFS= read -r f; do echo "--- $f ---"; cat "$f"; done
echo "=== skill descriptions ===" ; { [ -d "$P/.claude/skills" ] && grep -r "^description:" "$P/.claude/skills" 2>/dev/null; grep -r "^description:" ~/.claude/skills 2>/dev/null; } | sort -u
echo "=== STARTUP CONTEXT ESTIMATE ==="
echo "global_claude_words: $(wc -w < ~/.claude/CLAUDE.md 2>/dev/null | tr -d ' ' || echo 0)"
echo "local_claude_words: $(wc -w < "$P/CLAUDE.md" 2>/dev/null | tr -d ' ' || echo 0)"
echo "rules_words: $(find "$P/.claude/rules" -name "*.md" 2>/dev/null | while IFS= read -r f; do cat "$f"; done | wc -w | tr -d ' ')"
echo "skill_desc_words: $({ [ -d "$P/.claude/skills" ] && grep -r "^description:" "$P/.claude/skills" 2>/dev/null; grep -r "^description:" ~/.claude/skills 2>/dev/null; } | wc -w | tr -d ' ')"
python3 -c "
import json, sys
try:
d = json.load(open('$SETTINGS'))
except Exception as e:
msg = '(unavailable: settings.local.json missing or malformed)'
print('=== hooks ==='); print(msg)
print('=== MCP ==='); print(msg)
print('=== MCP FILESYSTEM ==='); print(msg)
print('=== allowedTools count ==='); print(msg)
sys.exit(0)
print('=== hooks ===')
print(json.dumps(d.get('hooks', {}), indent=2))
print('=== MCP ===')
s = d.get('mcpServers', d.get('enabledMcpjsonServers', {}))
names = list(s.keys()) if isinstance(s, dict) else list(s)
n = len(names)
print(f'servers({n}):', ', '.join(names))
est = n * 25 * 200
print(f'est_tokens: ~{est} ({round(est/2000)}% of 200K)')
print('=== MCP FILESYSTEM ===')
if isinstance(s, list):
print('filesystem_present: (array format -- check .mcp.json)')
print('allowedDirectories: (not detectable)')
else:
fs = s.get('filesystem') if isinstance(s, dict) else None; a = []
if isinstance(fs, dict):
a = fs.get('allowedDirectories') or (fs.get('config', {}).get('allowedDirectories') if isinstance(fs.get('config'), dict) else [])
if not a and isinstance(fs.get('args'), list):
args = fs['args']
for i, v in enumerate(args):
if v in ('--allowed-directories', '--allowedDirectories') and i+1 < len(args): a = [args[i+1]]; break
if not a: a = [v for v in args if v.startswith('/') or (v.startswith('~') and len(v) > 1)]
print('filesystem_present:', 'yes' if fs else 'no')
print('allowedDirectories:', a or '(missing or not detected)')
print('=== allowedTools count ===')
print(len(d.get('permissions', {}).get('allow', [])))
" 2>/dev/null || echo "(unavailable)"
echo "=== NESTED CLAUDE.md ===" ; find "$P" -maxdepth 4 -name "CLAUDE.md" -not -path "$P/CLAUDE.md" -not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null || echo "(none)"
echo "=== GITIGNORE ==="
_GITIGNORE_HIT=$(git -C "$P" check-ignore -v .claude/settings.local.json 2>/dev/null || true)
if [ -n "$_GITIGNORE_HIT" ]; then
_GITIGNORE_SOURCE=${_GITIGNORE_HIT%%:*}
case "$_GITIGNORE_SOURCE" in
.gitignore|.claude/.gitignore)
echo "settings.local.json: gitignored"
;;
*)
echo "settings.local.json: ignored only by non-project rule ($_GITIGNORE_SOURCE) -- add a repo-local ignore rule"
;;
esac
else
echo "settings.local.json: NOT gitignored -- risk of committing tokens/credentials"
fi
echo "=== HANDOFF.md ===" ; cat "$P/HANDOFF.md" 2>/dev/null || echo "(none)"
echo "=== MEMORY.md ===" ; cat "$HOME/.claude/projects/-$(pwd | sed 's|[/_]|-|g; s|^-||')/memory/MEMORY.md" 2>/dev/null | head -50 || echo "(none)"
echo "=== CONVERSATION FILES ==="
PROJECT_PATH=$(pwd | sed 's|[/_]|-|g; s|^-||')
CONVO_DIR=~/.claude/projects/-${PROJECT_PATH}
ls -lhS "$CONVO_DIR"/*.jsonl 2>/dev/null | head -10
echo "=== CONVERSATION EXTRACT (up to 3 most recent, confidence improves with more files) ==="
# Skip the active session, it may still be incomplete.
_PREV_FILES=$(ls -t "$CONVO_DIR"/*.jsonl 2>/dev/null | tail -n +2 | head -3)
if [ -n "$_PREV_FILES" ]; then
echo "$_PREV_FILES" | while IFS= read -r F; do
[ -f "$F" ] || continue
echo "--- file: $F ---"
head -c 2M "$F" | jq -r '
if .type == "user" then "USER: " + ((.message.content // "") | if type == "array" then map(select(.type == "text") | .text) | join(" ") else . end)
elif .type == "assistant" then
"ASSISTANT: " + ((.message.content // []) | map(select(.type == "text") | .text) | join("\n"))
else empty
end
' 2>/dev/null | grep -v "^ASSISTANT: $" | head -300 || echo "(unavailable: jq not installed or parse error)"
done
else
echo "(no conversation files)"
fi
echo "=== MCP ACCESS DENIALS ==="
ls -t "$CONVO_DIR"/*.jsonl 2>/dev/null | head -5 | while IFS= read -r F; do
head -c 1M "$F" | grep -Em 2 'Access denied - path outside allowed directories|tool-results/.+ not in ' 2>/dev/null
done | head -20
# --- Skill scan ---
# Exclude self by frontmatter name, stable across install paths.
SELF_SKILL=$( (grep -rl '^name: health$' "$P/.claude/skills" "$HOME/.claude/skills" 2>/dev/null || true) | grep 'SKILL.md' | head -1)
[ -z "$SELF_SKILL" ] && SELF_SKILL="health/SKILL.md"
echo "=== SKILL INVENTORY ==="
for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
[ -d "$DIR" ] || continue
find -L "$DIR" -name "SKILL.md" 2>/dev/null | grep -v "$SELF_SKILL" | while IFS= read -r f; do
WORDS=$(wc -w < "$f" | tr -d ' ')
IS_LINK="no"; LINK_TARGET=""
SKILL_DIR=$(dirname "$f")
if [ -L "$SKILL_DIR" ]; then
IS_LINK="yes"; LINK_TARGET=$(readlink -f "$SKILL_DIR")
fi
echo "path=$f words=$WORDS symlink=$IS_LINK target=$LINK_TARGET"
done
done
echo "=== SKILL FRONTMATTER ==="
for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
[ -d "$DIR" ] || continue
find -L "$DIR" -name "SKILL.md" 2>/dev/null | grep -v "$SELF_SKILL" | while IFS= read -r f; do
if head -1 "$f" | grep -q '^---'; then
echo "frontmatter=yes path=$f"
sed -n '2,/^---$/p' "$f" | head -10
else
echo "frontmatter=MISSING path=$f"
fi
done
done
echo "=== SKILL SYMLINK PROVENANCE ==="
for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
[ -d "$DIR" ] || continue
find "$DIR" -maxdepth 1 -type l 2>/dev/null | while IFS= read -r link; do
TARGET=$(readlink -f "$link")
echo "link=$(basename "$link") target=$TARGET"
if [ -d "$TARGET/.git" ]; then
REMOTE=$(git -C "$TARGET" remote get-url origin 2>/dev/null || echo "unknown")
COMMIT=$(git -C "$TARGET" rev-parse --short HEAD 2>/dev/null || echo "unknown")
echo " git_remote=$REMOTE commit=$COMMIT"
fi
done
done
echo "=== SKILL FULL CONTENT (sample: up to 5 skills, 80 lines each) ==="
{ for DIR in "$P/.claude/skills" "$HOME/.claude/skills"; do
[ -d "$DIR" ] || continue
find -L "$DIR" -name "SKILL.md" 2>/dev/null | grep -v "$SELF_SKILL"
done
} | head -5 | while IFS= read -r f; do
echo "--- FULL: $f ---"
head -80 "$f"
done
注意事项
解读步骤 1 的输出前,先检查以下已知故障模式。
数据收集静默失败
- 未安装
jq:对话提取会输出(unavailable: jq not installed or parse error)。BEHAVIOR 部分将为空——将其视为 [INSUFFICIENT DATA],而非发现项。 python3不在 PATH 中:所有 MCP/hooks/allowedTools 部分都会输出(unavailable)。如果数据源本身失败,不要标记这些区域。- 不存在
settings.local.json:hooks、MCP 和 allowedTools 均会显示(unavailable)。仅使用全局设置的项目通常如此——这不属于配置错误。
MEMORY.md 路径构造
- 路径通过对
pwd使用sed 's|[/_]|-|g'构造。特殊字符可能导致项目键错误。如果 MEMORY.md 显示(none),但用户提到了之前的会话,请先手动验证路径,再标记为 [!]。
对话提取范围
- 仅抽取最近的 3 个
.jsonl文件,跳过当前会话。少于 3 个文件的发现可信度较低——务必标记为 [LOW CONFIDENCE]。
MCP 令牌估算
- 假设每个服务约有 25 个工具,每个工具约需 200 个令牌。工具数量远多或远少的服务会导致估算大幅偏高或偏低。此估算仅供参考,并非精确值。
级别误判边缘情况
- bash 代码块会排除
node_modules/、dist/和build/,但不会排除所有生成目录。包含.next/、__pycache__/或.turbo/输出的单体仓库可能会导致文件数虚高,进而被误判为 COMPLEX 级别。如果级别看起来不合理,请手动复核。
步骤 2:按级别调整分析深度
步骤 1 完成后,先输出摘要行,再输出步骤指示符:
Tier: {SIMPLE/STANDARD/COMPLEX} -- {file_count} files · {contributor_count} contributors · CI: {present/absent}
Step 2/3: {SIMPLE: "Analyzing locally" | STANDARD/COMPLEX: "Launching parallel analysis agents"}
SIMPLE:在上方输出“正在本地分析”。不要启动子 Agent。根据步骤 1 的结果进行分析,优先检查核心配置;除非证据明显,否则跳过需要大量交叉验证的对话分析。
STANDARD/COMPLEX:在上方输出“正在启动并行分析 Agent”,然后列出覆盖范围:
· Agent 1: CLAUDE.md, rules, skills, MCP context + security scan
· Agent 2: hooks, allowedTools, behavior patterns, three-layer defense
并行启动两个子 Agent。将所有数据直接粘贴到内容中——不要传递文件路径。
Agent 1——上下文与安全审计(无需对话内容)
阅读此 skill 所在目录中的 agents/agent1-context.md。该文件说明应粘贴哪些步骤 1 的部分内容,并提供完整的审计清单。
Agent 2——控制与行为审计(使用对话证据)
阅读此 skill 所在目录中的 agents/agent2-control.md。该文件说明应粘贴哪些步骤 1 的部分内容,并提供完整的审计清单。
步骤 3:综合并呈现
撰写报告前,先使用输出语言输出一行进度信息:
Step 3/3: Synthesizing report
汇总本地分析结果和所有 Agent 的输出,生成一份报告:
健康报告:{project}({tier} 级别,{file_count} 个文件)
✓ 通过
以精简表格呈现通过的检查。仅包含与检测到的层级相关的检查。最多 5 行。有问题的检查不显示。
| 检查 | 详情 |
|---|---|
| settings.local.json 已加入 gitignore | 正常 |
| 没有嵌套的 CLAUDE.md | 正常 |
| Skill 安全扫描 | 未发现问题 |
☻ 严重——立即修复
违反规则、缺少验证定义、危险的 allowedTools、MCP 开销 >12.5%、必需路径 Access denied、活跃的缓存破坏因素,以及安全问题。
◎ 结构性——尽快修复
应放在其他位置的 CLAUDE.md 内容、缺少 hooks、Skill 描述过长、关键规则仅有单层防护、模型切换、验证器缺口、子 Agent 权限缺口,以及 Skill 结构问题。
○ 增量改进——有则更好
可新增的模式、应移除的过时内容、全局与本地的放置方式、上下文整洁度、采用 HANDOFF.md、Skill 调用调优,以及来源信息问题。
如果三个问题部分都为空,请用输出语言输出一条简短提示,例如:✓ 所有相关检查均已通过。无需修复。
非目标
- 未经确认,绝不自动应用修复。
- 绝不对简单项目执行复杂层级的检查。
- 只标记问题,不替代架构判断。
**停止条件:**报告结束后,用输出语言询问:
“要我起草这些更改吗?我可以分别处理每一层:全局 CLAUDE.md / 本地 CLAUDE.md / hooks / skills。”
未经明确确认,不得进行任何编辑。