0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CLAUDE.mdを増やす前に、AI agentの禁止事項をhookとCIへ移す

0
Posted at

CLAUDE.md に「変更後は必ずtestする」と追記した。それでもcoding agentが検証を飛ばす。次は「test結果を確認してから完了報告する」と強く書く。しばらくすると、別の経路で同じ事故が起き、文書の語気だけが強くなる。

CLAUDE.md が役に立たないわけではない。package managerや設計方針を伝えるには便利だ。ただし、agentが読むcontextと、実行を止める仕組みは別にしたほうがいい。副作用はtool実行前のhook / permissionで止め、lintやtestは実行後のgateで落とす。

ここで扱うのはhookの設定リファレンスではない。既存のCLAUDE.mdにあるMUSTNEVERを棚卸しして、ルールを置くlayerを決め直す。

長いinstructionは、強制設定ではない

長い指示書に従う能力を測るHANDBOOK.md benchmarkでは、65のtaskと824の機械判定基準が使われた。handbookは20〜124ページあり、strict pass@1の最高値は36.2%だった。

この数字はbenchmark条件での結果であり、手元のrepositoryにおけるClaude Codeの成功率ではない。それでも、「書いておけば毎回守られる」を前提に設計するのは危ない、と考える材料にはなる。

Claude Codeの公式ドキュメントでも、CLAUDE.mdはproject memoryとしてcontextに読み込まれる。enforced configurationではない。実行前に止めたい操作にはPreToolUse hookやpermissionという別の境界が用意されている。

CLAUDE.mdの短さを競う話ではない。文書に強制まで背負わせず、担当範囲をcontextへ戻す。

まずルールを3つのlayerに分ける

棚卸しでは、ルールの文章を直す前に置き場所を決める。

layer 役割 失敗したとき
context 判断材料を渡す pnpmを使う、directoryの責務、設計上の理由 agentが選択時に参照する
pre-tool gate 戻しにくい副作用を実行前に止める repository外へのwrite、未承認publish tool callをblockする
post-run gate 結果を機械判定する lint、typecheck、test、artifact確認 commandをnon-zeroで終える

迷ったら、この程度の分類関数に落としてみると整理しやすい。

type RuleLayer = 'context' | 'pre-tool' | 'post-run'

type Rule = {
  mustStopBeforeSideEffect: boolean
  canCheckAfterRun: boolean
}

function chooseLayer(rule: Rule): RuleLayer {
  if (rule.mustStopBeforeSideEffect) return 'pre-tool'
  if (rule.canCheckAfterRun) return 'post-run'
  return 'context'
}

たとえば「pnpmを使う」は判断時に必要なのでcontextに残す。「repository外へ書き込まない」は、書き込んだあとに検査しても遅いためpre-tool gateへ移す。「type errorを残さない」は実行結果を判定できるのでpost-run gateへ移す。

「このUIで迷わないか」のような条件はCIだけでは判定しにくい。そこは人が見る。何でも自動gateへ押し込むと、通過条件だけを満たした使いにくい実装ができる。

CLAUDE.mdには判断材料を残す

CLAUDE.mdから禁止事項を全部消す必要はない。小さなTypeScript repositoryなら、たとえば次の情報を残す。

# Project context

- Package managerはpnpmを使う
- 実装はsrc/、testはtests/に置く
- 変更後の共通確認commandはpnpm verify:agent
- dist/はbuild outputなので直接編集しない
- Architectureの背景はdocs/architecture.mdを参照する

ここには、agentが実装方法を選ぶための情報がある。長い設計説明は別ドキュメントへ分け、入口だけを置けばいい。

逆に、次のような文が増えていたら移動候補だ。

- NEVER write outside this repository
- MUST run lint, typecheck, test and build
- NEVER report completion when build output is missing

1行目はpre-tool gate、残りはpost-run gateで扱える。強調語をさらに足すより、失敗時に処理が止まる場所を作る。

副作用はtool callの前で止める

実行後に戻しにくい操作は、agentが指示を思い出すかどうかに任せない。hook / permission側でtool inputを見て、許可か拒否かを返す。

以下はClaude Code固有のhook schemaではなく、hookから呼ぶpolicy関数の概念例だ。

import path from 'node:path'

type ToolAction =
  | { kind: 'write'; target: string }
  | { kind: 'publish'; target: string }
  | { kind: 'read'; target: string }

type Decision =
  | { allow: true }
  | { allow: false; reason: string }

function isOutsideRepository(repoRoot: string, target: string): boolean {
  const absoluteTarget = path.isAbsolute(target)
    ? target
    : path.resolve(repoRoot, target)
  const relative = path.relative(repoRoot, absoluteTarget)
  return relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
}

export function decide(action: ToolAction, repoRoot: string): Decision {
  if (action.kind === 'write' && isOutsideRepository(repoRoot, action.target)) {
    return { allow: false, reason: 'repository外へのwriteは許可されていません' }
  }

  if (action.kind === 'publish') {
    return { allow: false, reason: 'publishには人間の承認が必要です' }
  }

  return { allow: true }
}

本番ではshell文字列を雑な正規表現で判定するより、tool名と正規化済みinputを使う。巨大なdeny listから始める必要もない。事故履歴を見て、戻すコストが高い操作を一つ選ぶほうが試しやすい。

hookの設定形式は更新される可能性があるため、matcherや返却形式は利用中のClaude Code公式ドキュメントに合わせる。ここで固定したいのはschemaではなく、「副作用より前で判定する」という責務だ。

完了条件は1本のcommandへ寄せる

小さなTypeScript repositoryを例に、local agentとCIが同じcommandを呼ぶようにする。

{
  "scripts": {
    "verify:artifacts": "node scripts/verify-artifacts.mjs",
    "verify:agent": "pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm verify:artifacts && git diff --check"
  }
}

dist/index.jsが必要なprojectなら、artifact checkも小さなscriptにできる。

// scripts/verify-artifacts.mjs
import { stat } from 'node:fs/promises'

const artifact = new URL('../dist/index.js', import.meta.url)
const result = await stat(artifact).catch(() => null)

if (!result || !result.isFile() || result.size === 0) {
  throw new Error('dist/index.js is missing or empty')
}

CI側で別の検証手順を再実装しない。

- name: Verify agent changes
  run: pnpm verify:agent

agentが「lintもtestも通りました」と書いても、それだけでは完了条件にしない。runnerのexit status、変更file、必要なartifactを確認する。summaryは証拠へ移動する入口として使う。

agent_result:
  command: pnpm verify:agent
  exit_code: 0
  changed_files:
    - src/index.ts
    - tests/index.test.ts
  artifacts:
    - dist/index.js

もちろん、このYAMLをagent自身が書いただけでは証明にならない。CI logや実fileから生成・照合できる項目だけをreceiptとして残す。

正常系だけで終わらせない

gateを追加したら、正常系が通るだけで満足せず、狙った場所で落ちることも確認する。

sample repositoryでは次の順で試せる。

  1. 変更前のbranchでpnpm verify:agentが通ることを確認する。
  2. smoke test用fileへ意図的なtype errorを入れ、commandがnon-zeroになることを確認する。
  3. type errorを戻し、同じcommandが通ることを確認する。
  4. policy関数へrepository外の架空pathを渡し、実際にはwriteせずにdenyになることをunit testする。
  5. CIでもpnpm verify:agentを実行し、localと同じ結果になるかを見る。

pre-tool gateのtestで危険なcommandを本当に実行する必要はない。判定関数へ無害なinputを渡せば、tool実行前に止まる経路を確認できる。

import path from 'node:path'
import { expect, test } from 'vitest'
import { decide } from '../scripts/tool-policy'

test('repository外へのwriteを拒否する', () => {
  const repoRoot = process.cwd()
  const outside = path.join(repoRoot, '..', 'blocked-smoke-test.txt')

  expect(decide({ kind: 'write', target: outside }, repoRoot)).toEqual({
    allow: false,
    reason: 'repository外へのwriteは許可されていません',
  })
})

こうしておけば、deny条件を変更したときもreviewしやすい。permission設定を目視しただけで終わらせず、block path自体をrepositoryのtest対象にできる。

最初は一つの事故だけ移す

CLAUDE.mdを一気に全面改稿すると、どの変更が効いたか分からなくなる。

最近起きた事故を一つ選び、そのルールをどこへ置くか決める。gateを追加したら、正常系と意図的な失敗を一度ずつ通す。最初の変更はそれで十分だ。

coding agentの指示違反に対して毎回文章を足しても、事故を止める場所は増えない。CLAUDE.mdには判断材料を残し、止めたい副作用はhook / permissionへ、機械で確認できる完了条件はCIとartifact checkへ移す。次に同じ事故が起きたとき、追記する文言ではなく、止めるlayerを選べるようになる。

参考資料

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?