skill-comply: Automated Compliance Measurement
Measures whether coding agents actually follow skills, rules, or agent definitions by:
- Auto-generating expected behavioral sequences (specs) from any .md file
- Auto-generating scenarios with decreasing prompt strictness (supportive → neutral → competing)
- Running
claude -pand capturing tool call traces via stream-json - Classifying tool calls against spec steps using LLM (not regex)
- Checking temporal ordering deterministically
- Generating self-contained reports with spec, prompts, and timelines
Supported Targets
- Skills (
skills/*/SKILL.md): Workflow skills like search-first, TDD guides - Rules (
rules/common/*.md): Mandatory rules like testing.md, security.md, debugging.md - Agent definitions (
agents/*.md): Whether an agent gets invoked when expected (internal workflow verification not yet supported)
When to Activate
- User runs
/skill-comply <path> - User asks "is this rule actually being followed?"
- After adding new rules/skills, to verify agent compliance
- Periodically as part of quality maintenance
Usage
# 前提: scripts/ と pyproject.toml はこのスキルのディレクトリにあり、
# `python -m scripts.run` の解決は cwd 依存のため、まずスキルディレクトリへ cd する
# (`uv run --project` だけでは module 解決できないことを 2026-07-13 に実測確認)
cd ~/.claude/skills/skill-comply
# Full run
uv run python -m scripts.run ~/.claude/rules/common/testing.md
# Dry run (no cost, spec + scenarios only)
uv run python -m scripts.run --dry-run ~/.claude/skills/search-first/SKILL.md
# Custom models
uv run python -m scripts.run --gen-model haiku --model sonnet --classifier-model sonnet <path>
# 直列に戻す(レートリミットに当たったとき)
uv run python -m scripts.run --concurrency 1 <path>
# Bash を要する spec のみ (既定は off — 下の「信頼境界」を読んでから)
uv run python -m scripts.run --allow-bash <path>
# 保存済み spec を再利用して run 間比較 (LLM 再生成をスキップ)
uv run python -m scripts.run --spec results/<skill-name>.spec.yaml <path>
spec の固定と run 間比較: spec は「試験問題」。LLM 生成のたびに required steps 数も
順序制約も変わるため、生成された spec は自動で results/<skill-name>.spec.yaml に保存される
(gitignore 対象外 — version 管理できる)。同じ skill を再測定するときは --spec でこれを
読み込むと問題文が固定され、スコアが比較可能になる。次回の生成 run は同名ファイルを
上書きするので、比較対象として残したい spec は別名でコピーしておく。
なお scenario prompt は引き続き毎回 LLM 生成で変動する — run 間比較の固定は spec までで、
scenario の非決定性は現状スコープ外。
実行時間と進捗の見かた
3 つのシナリオは互いに独立(別プロンプト・別 sandbox・別プロセス)なので、 既定で 3 本同時に走る。待ち時間は 3 本の合計ではなくいちばん遅い 1 本になる。 分類(採点)もシナリオごとなので一緒に並ぶ。spec 生成 → シナリオ生成の 2 段は 前段の出力を次段が使うため直列のまま。
並列化してもスコアとレポートは変わらない。完了順は進捗表示にしか使わず、
レポートは必ず supportive → neutral → competing の順に組み立てる。
--concurrency 1 で完全に直列へ戻せる。
進捗は stderr、結果は stdout に出る。 これは事故ではなく分担で、 2026-08-01 の実測にもとづく(→ ADR-0029):
# 進捗が見える。stdout だけが tail に入り、stderr は端末へ直接届く
uv run python -m scripts.run <path> | tail -40
# 進捗もログに残したい
uv run python -m scripts.run <path> 2>&1 | tee run.log
2>&1 | tail -40 にすると何も見えなくなる。 tail は -f なしだと
「最後の N 行」を出す道具で、入力が終わるまで 1 行も出せない。
これは tail の性質なので、python -u でも flush でも直らない。
途中経過を見たいときは tail でなく tee を使う。
シナリオが 1 本でも失敗したとき(子プロセスの異常終了など)は、 そのシナリオを 0% として数えず「測定失敗」として stderr に出し、終了コード 1 を返す。 残りのシナリオのレポートは通常どおり保存される。
測定対象の種別と、測れるもの
対象がどこにあるかで、子から見えるかどうかが変わる。見えないまま測ると、
skill 遵守ではなく「skill を持たないエージェントの素の挙動」を測ることになる。
2026-08-02 まではそれが黙って起きていた(実例: project skill の run で
Skill(...) → Unknown skill のまま 75% / 50% / 25% が出ていた)。
| 対象 | 子から見えるか | 測定 |
|---|---|---|
global skill(~/.claude/skills/) |
見える | そのまま測れる |
project skill(repo の .claude/skills/) |
見えない | sandbox に配置してから測る(下記の 2 層) |
| rule / agent 定義 / 素の .md | skill ではない | Skill 呼び出しは期待しない |
project skill は 2 層で測る。どちらで測ったかはレポートの Summary に必ず出る — Tier 1 の 75% と Tier 2 の 75% は別物なので、混同できないようにする。
- Tier 1(既定、flag 不要) — frontmatter の
nameとdescriptionだけ本物で、 本文は無害な stub を置く。「skill に手を伸ばしたか」を測る。 発見と起動はname/descriptionで決まり本文は関与しないので、これで足りる - Tier 2(
--load-target-skill) — 本物の本文を置く。「手順に従うか」を測る。 監査対象の本文が無人の子への指示になるので opt-in。SKILL.md1 ファイルのみを 写し、ディレクトリは写さない(references/を持つ skill は Tier 2 で短く測れる — 黙った穴より、見える制限を選ぶ)
Tier 1 は測定の正しさのための層であって、封じ込めの層ではない。 存在理由は 「project skill を発見可能にして、素の挙動でなく遵守を測れるようにする」こと。
description は発見に必要なので必ず子に届く。上限 500 文字は分量の上限であって
能力の上限ではない — 500 文字は指示文として十分すぎる長さで、Read / Write / Edit /
Glob / Grep を持つ子に有効な命令を書ける。Tier 1 と Tier 2 の差は
**「手順が届かない」であって「指示が届かない」**ではない。
--load-target-skill と --allow-bash の併用は、untrusted な文書に payload と
インタープリタの両方を渡す組み合わせなので警告が出る。
信頼境界 — 監査対象ファイルは untrusted
このツールは、あなたが書いたとは限らない .md を読んで LLM にシナリオを書かせ、 それを別のエージェントに実行させる。 監査対象がそのまま生成器への入力になるので、 対象ファイルの本文は untrusted data として扱う(2026-07-25 security scan F2/F3/F4/F18)。
setup_commandsは実行されない。mkdir/touchの 2 語彙だけを pathlib で 解釈し、パスは sandbox 内に解決されることを検証する。それ以外は拒否して stderr に 出す。以前はshlex.split+subprocessだったため、community 由来スキル 1 本で ホスト上の任意コマンド実行になっていた- パス検証は
..を先に潰してから symlink を解決する。 2026-08-01 まで、存在しない 要素の後ろに置かれた..(a/../../elsewhere/x)が解決されずに残り、Path.parentsが それをただのディレクトリ名として扱って sandbox 外への作成を通していた。 sandbox は直前に消して作り直されるので最初の要素は必ず存在せず、抜け道は常に開いていた - 子のツールは
permissions.denyで外す。--allowedToolsでは外れない。claude --helpの言うとおり--allowedToolsは「allow するツール名の列」=自動承認 リストであって、載せなかったツールは消えない。2026-08-02 に Claude Code 2.1.220 で 実測:--allowedTools "Read,Glob,Grep"だけの子が Bash を呼び、uname -srが ホスト上で実行された。--permission-mode(manual / dontAsk / acceptEdits)でも--setting-sources projectでも変わらない。--settingsのpermissions.denyでBash/Agent/Workflow/ToolSearch/ScheduleWakeupを外す(正本:scripts/child_settings.py)。 Bash 以外も外すのは、AgentとWorkflowがこのコードが制御しないツール集合を持つ サブエージェントを生み、ToolSearchが user 設定から継承した MCP の面 (メール・ドライブ・カレンダー・ブラウザ)を必要に応じて読み込むため。--allow-bashは deny から Bash を外す形で効く cwdと--add-dirはアクセスを広げるもので、閉じ込めない- 生成器プロンプトでは対象文書を nonce 区切りで隔離し、data であって指示ではないと
明示している。固定区切り(
---)は markdown frontmatter が再現できてしまう - sandbox は 1 シナリオ 1 個、かつ 1 実行 1 根。ディレクトリ名は LLM が生成した
scenario id に由来するので、重複していたら実行前に検出して別名にする。直列なら重複は
無害だが、並列では片方の sandbox 作成(作る前に消す)が走行中のもう片方を消してしまう。
この一意化はプロセス内でしか効かないので、根を実行単位で分ける —
/tmp/skill-comply-sandbox/run-<pid>/<id>。同じ id を生成した別の run が 互いの走行中 sandbox を消さない(2026-08-17、scripts/runner.py: sandbox_run_root)。SANDBOX_BASE自体が symlink(共有ホスト・CI の scratch 領域)のときは根の計算で 1 回だけ解決して追従する — 封じ込め判定を毎回 link 越しにやり直さない <sandbox>/.claude/と<sandbox>/.git/はツール専有。監査対象由来の指定は受け付けない。 sandbox は子にとってのプロジェクトルートで、そこでは一部のファイル名が 「置いてあるだけ」ではなく読み込まれる。実測(2026-08-02): 一度も信頼していない workspace でも<sandbox>/.claude/settings.jsonのhooks.SessionStartは 無言でホスト上のコマンドを実行した(同じファイルのpermissions.allowは 「信頼されていない」と明示的に拒否されるのに)。<sandbox>/CLAUDE.mdも読まれて従われる。 子自身の Write は substrate が止めるが、このツールの pathlib 書き込みは止まらないので ここで塞ぐ。CLAUDE.md/AGENTS.md/.mcp.json/settings.json/settings.local.json/.gitignoreは深さを問わず拒否。 判定は case-fold する — APFS は case-insensitive なので.CLAUDE/Settings.jsonは 書けてしまえば.claude/settings.jsonとして読まれる(2026-08-02 にすり抜けを実測して修正)。.git/を含めるのは、_setup_sandboxがgit initを先に走らせるので書き込み可能で、core.fsmonitor/core.pager/alias.*が実行ビット無しで git が実行する設定文字列 だから(実測:files:で.git/configを置き、git statusでホスト上のコマンドが走った。 Claude Code は workspace で git を実行するので、子の Bash を塞いでいても発火する)。.gitignoreは実行ではなく測定の破壊 — Grep は従い Glob は従わないので、文書が 自分のフィクスチャを detector の期待するツールから隠せる。 閉じ込められていることと不活性であることは別の性質で、_containedが答えるのは前者だけ
信頼できない .md を測るときは --dry-run で生成されたシナリオを先に読むこと。
--dry-run は spec の step に加えて、攻撃者が制御しうる 3 つのフィールドを全文出す —
無人の子に渡される prompt、ファイルシステムを触る setup_commands と files:。
Models
| Stage | Default | Why |
|---|---|---|
--gen-model |
haiku |
Spec / scenario generation. Short prompts, fast. |
--model |
sonnet |
Scenario execution (the agent under test). haiku / sonnet / opus / fable を指定可。 |
--classifier-model |
sonnet |
Trace classification. Haiku times out on long traces (50+ events) and abstract specs (e.g. contemplative-axioms). Sonnet handles the load with a 300s timeout. |
Key Concept: Prompt Independence
Measures whether a skill/rule is followed even when the prompt doesn't explicitly support it.
Report Contents
Reports are self-contained and include:
- Expected behavioral sequence (auto-generated spec)
- Scenario prompts (what was asked at each strictness level)
- Compliance scores per scenario
- Tool call timelines with LLM classification labels
Advanced (optional)
For users familiar with hooks, reports also include hook promotion recommendations for steps with low compliance. This is informational — the main value is the compliance visibility itself.