1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI エージェントの設定を、機能ごとに ON / OFF する CLI「llmtpl」をつくった

1
Last updated at Posted at 2026-08-19

AI エージェントに機能をひとつ足すと、設定が何か所にも散ります。Claude Code なら、AI への指示・規約・スクリプト・その登録・skill …と、置き場が種類ごとに分かれているからです。

足すときはいいんです。困るのはやめるとき。散らばった置き場を、手で戻して回ることになります。

散らばるのが避けられないなら、せめて 1 か所に畳んで、1 行で出し入れできないか。そう考えて、散らばりを「機能ごとのディレクトリ」ひとつに畳むツールを書きました。設定ファイルにこう書けば、

logcheck = true

AI への指示も、規約も、スクリプトも、その登録も、まとめて入ります。false に変えれば、まとめて消えます。

go install github.com/ryokwkm/llmtpl@latest   # 要 Go 1.24+

対象読者: Claude Code などの AI エージェントに機能を足したり外したりしている人。複数リポジトリで同じ設定を使い回している人には、とくに効くはずです。

動作環境: 記事内の出力はすべて実測値です。

  • macOS(Linux も対応。symlink を使うため Windows は未対応)
  • Claude Code(設定レイアウトの例として。.claude 決め打ちではありません)
  • Go 1.24+(インストールにだけ必要)

機能をひとつ足すと、置き場が種類ごとに散る

エージェントに「作業ログを自動で検証する」機能を足すとします。たとえばこの機能だと、置き場は 4 か所になります。

.claude/CLAUDE.md      ① AI が常に読む指示
.claude/rules/         ② 長い規約の本体(① は毎回読まれるので短く保ち、詳細はこちらへ逃がす)
.claude/hooks/         ③ 決まったタイミングで自動実行されるスクリプト
.claude/settings.json  ④ ③ をいつ走らせるかの登録

置き場の種類はこれで全部ではありません。 skill を配る機能なら .claude/skills/、サブエージェントを足すなら .claude/agents/ が増えます。数は機能ごとに変わります。共通しているのは、置き場が種類ごとに分かれていて、機能 1 個ぶんがそこへ散ることのほうです。

置くだけならどれも数行です。問題はやめるとき。この 4 か所を手で戻すことになって、1 か所忘れると事故ります。

  • ① だけ残ると → AI が存在しないルールに従おうとする
  • ③④ だけ残ると → 誰も読まない検証がずっと走り続ける

しかもこれをリポジトリごとにやります。「この機能はあっちでは要るけど、こっちでは要らない」を、機能 3 つ × リポジトリ 5 つぶん手で持ち始めたあたりで、私は破綻しました。

そこで、散らばるぶんをひとつのディレクトリに畳んで、設定ファイルの 1 行で出し入れすることにしました。


散らばるぶんを、ひとつのディレクトリに畳む

置く

さっきの 4 か所を、そのまま 1 つのディレクトリに入れます。

demo/
├── llm-tpl/                              ← 機能のかたまりを並べる場所
│   └── logcheck/                         ← ★ ディレクトリ名がフラグ名
│       └── .claude/                        中は ↓ の受け取る側と同じ形
│           ├── CLAUDE.md.tmpl            ① AI への指示
│           ├── rules/format.md           ② ルールファイル
│           ├── hooks/verify.sh           ③ スクリプト
│           └── settings.json.tmpl        ④ hook の登録
│
└── proj/                                 ← 設定を受け取るプロジェクト
    ├── llmtpl.conf                       logcheck = true
    └── .claude/
        ├── CLAUDE.md.tmpl                このプロジェクトのベース
        └── settings.json.tmpl            このプロジェクトのベース

logcheck というディレクトリ名が、そのままフラグ名になります。 フラグ一覧をどこかに書いて対応付ける作業はありません。ディレクトリを作れば、その名前のフラグが生まれます。この機能 1 個ぶんのディレクトリを、以降「バンドル」、受け取る側の proj/ を「ターゲット」と呼びます。覚える言葉はこの 2 つだけです。

バンドルの中は、プロジェクトの中と同じ形にします。 置いたものが、そのまま同じ位置へ重なる。それだけです。.claude/rules/ に置いたものは proj/.claude/rules/ へ、ルート直下に置いたもの(AGENTS.md.tmpl など)は proj/ 直下へ届きます。

ターゲットの目印は llmtpl.conf 1 つで、それがあるディレクトリがプロジェクトルートです。バンドルを並べる llm-tpl/ をその外に置くのは、あとで複数のターゲットが同じ置き場を共有するためです。

中身は素朴です。拡張子は .tmpl ですが、中身はただの Markdown / JSON で構いません。合成される ① CLAUDE.md.tmpl と ④ settings.json.tmpl を見てみます(② と ③ はそのまま配られるだけなので、中身は何でも構いません)。

llm-tpl/logcheck/.claude/CLAUDE.md.tmpl
## 作業ログの検証

- 作業を終えたら `log/` の書式を検証する。規約は `.claude/rules/logcheck/format.md`- 手で確かめるときは `.claude/hooks/verify.sh` を実行する。
llm-tpl/logcheck/.claude/settings.json.tmpl
{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/verify.sh" }] }
    ]
  }
}

Stop は「AI が応答を終えたタイミング」を指すイベント名です。書けるのは hook の登録だけではありません。permissionsenv など、settings.json に書けるキーはそのまま置けます。

ターゲット側の 2 つの .tmpl は、バンドルの断片を受け取るベースです。そのプロジェクト固有の内容だけを書いておきます。ここでは CLAUDE.md.tmpl がこの 2 行、

proj/.claude/CLAUDE.md.tmpl
# proj のAI指示サンプル

- Go で書く。

settings.json.tmpl{"model": "opus"} だけです。

そして proj/llmtpl.conf に 1 行。

proj/llmtpl.conf
logcheck = true

ON にする

$ cd demo/proj && llmtpl apply

▸ proj  [ON: logcheck]
  ✅ 生成 .claude/CLAUDE.md
  ✅ 生成 .claude/settings.json
  🔗 リンク .claude/hooks/verify.sh -> ../../../llm-tpl/logcheck/.claude/hooks/verify.sh
  🔗 リンク .claude/rules/logcheck -> ../../../llm-tpl/logcheck/.claude/rules

叩くのは自分のプロジェクトの中です(既定は cwd とその配下)。バンドル置き場の llm-tpl/ は、実行した場所から親ディレクトリをさかのぼって自動で見つかります(この例では proj/ のひとつ上にあります)。親方向に無い場所へ置きたいときは、llmtpl.confbundle_root = <パス> と書いて明示できます。

4 つとも入りました。生成された CLAUDE.md をベース(さっきの 2 行)と比べると、バンドルの指示が後ろに足されています(1 行目は llmtpl が入れる生成マーカです)。

CLAUDE.md
+<!-- GENERATED — 直接編集禁止。原本: proj/.claude/CLAUDE.md.tmpl(編集は原本 → llmtpl apply で反映) -->
 # proj のAI指示サンプル

 - Go で書く。
+
+## 作業ログの検証
+
+- 作業を終えたら `log/` の書式を検証する。規約は `.claude/rules/logcheck/format.md`+- 手で確かめるときは `.claude/hooks/verify.sh` を実行する。

settings.json はターゲット自身の model とバンドルの hooks がマージされます。キーの重なりを解いて 1 つの JSON にするのが、symlink では代われない部分です。

settings.json
{
+  "hooks": {
+    "Stop": [
+      {
+        "hooks": [
+          {
+            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/verify.sh",
+            "type": "command"
+          }
+        ]
+      }
+    ]
+  },
  "model": "opus"
}

rules/hooks/ は symlink です。実体はバンドル側にあるままなので、コピーは増えません。

いま起きたことを整理すると、同じ 1 つのディレクトリから、3 通りの届き方をしています。

.md は末尾に足すだけ、.json はキーを見て混ぜる、ディレクトリはリンクを張る。扱いを決めているのは拡張子だけで、ファイル名を llmtpl が知っているわけではありません。

置く位置も同じで、llmtpl は .claude という名前を特別扱いしません。バンドルの .claude/ に置いたものがプロジェクトの .claude/ へ行くのは、同じ位置へ重なるという規則の結果にすぎません。だから AGENTS.md.tmpl をバンドルのルート直下に置けば proj/AGENTS.md ができるし、.cursor/ を作れば proj/.cursor/ へ届きます。

OFF にする

llmtpl.conflogcheck = false に書き換えて、もう一度実行します。

$ llmtpl apply

▸ proj  [ON: (なし)]
  ✅ 生成 .claude/CLAUDE.md
  ✅ 生成 .claude/settings.json
  ✂️  リンク解除 .claude/hooks/verify.sh
  ✂️  リンク解除 .claude/rules/logcheck

足された分が、そのまま引かれます。CLAUDE.md の差分はこれだけ。

CLAUDE.md
 # proj のAI指示

 - Go で書く。
-
-## 作業ログの検証
-
-- 作業を終えたら `log/` の書式を検証する。規約は `.claude/rules/logcheck/format.md`-- 手で確かめるときは `.claude/hooks/verify.sh` を実行する。

settings.json からは hooks が丸ごと消えて "model": "opus" だけになり、symlink も 2 本とも外れました。プロジェクト固有の内容はそのまま残ります。

冒頭の「手で戻して回る」が、1 行の書き換えになりました。

それ、Stow でよくない?

ここまでの動きは、要するに「バンドルの中身を配って、外す」だけです。私も最初は、symlink を張る既存の道具で足りると思っていました。実際に要ったのは、さっきの図の 3 本の矢印です。

  1. ファイルやディレクトリをまとめて配る・外すhooks/verify.shrules/
  2. 1 つのファイルの中へ数行を差し込む・抜くCLAUDE.md
  3. 1 つの JSON の別々のキーへ、複数の機能が書き込むsettings.json

GNU Stow は 1 番だけ。ファイルより細かい単位は扱いません。chezmoi は 2 番まで。ただし配布の単位が「原本 1 つ → 配布先 1 つ」なので、{{if}} の分岐が機能ごとではなく配布先ファイルのほうに集まります。3 番は自分で書き足すことになります。

足りなかったのは 2 番と 3 番でした。欲しかったのは、さっき動かしたこの状態です。

スクリプトの実体と、それを AI に知らせる数行と、それを自動で走らせる JSON の一片が、同じディレクトリで一緒に増減する。


複数のリポジトリへ配る

上のデモは 1 つのディレクトリで完結していました。実際にはバンドル置き場を 1 か所だけ作って、各リポジトリはそこから受け取ります。

~/.config/llmtpl/            ← バンドル置き場。ここだけが 1 か所
└── logcheck/.claude/          さっきの 4 ファイルがそのまま入っている

~/src/proj-a/                ← 受け取る側。増えるのはこの 3 ファイルだけ
├── llmtpl.conf                logcheck = true
└── .claude/
    ├── CLAUDE.md.tmpl         ← いまの CLAUDE.md をリネームしたもの
    └── settings.json.tmpl     ← いまの settings.json をリネームしたもの

~/src/proj-b/                同じ形(logcheck = true)
~/src/proj-c/                同じ形(logcheck = false)

2 つ目のバンドル audit を足すと、こうなります。

実体は左に 1 つあるだけで、右の 3 つが持っているのは「どのフラグを立てるか」の 1 行だけです。配るときはリポジトリを並べて 1 回叩きます。

$ cd ~/src && llmtpl apply proj-a proj-b proj-c

張られるのは相対 symlinkなので、ホームの位置やユーザー名が違うマシンでも同じリンクが通ります。

いま何がどこで ON なのかは status で一覧できます。

$ llmtpl status proj-a proj-b proj-c

対象 3 件
バンドルルート: /Users/me/.config/llmtpl
ターゲット  audit  logcheck
proj-a      ON     ON
proj-b      -      ON
proj-c      ON     -

効いてくるのは掛け算になってからです。 機能 3 つ × リポジトリ 5 つを手で持ち始めたあたりから、この表が頭の中に無くなります。


いま使っているリポジトリへ入れる

すでに CLAUDE.mdsettings.json があるリポジトリが前提です。5 手で終わります。

  1. .claude/CLAUDE.md.claude/CLAUDE.md.tmpl.claude/settings.json.claude/settings.json.tmplリネーム
  2. リポジトリのルートllmtpl.conf を置いて、要るバンドルを 1 行書く
  3. llmtpl apply --dry-run で予定を見る
  4. 問題なければ llmtpl apply
  5. 張られた symlink を .gitignore に足す(コミットすると、llmtpl を持っていない人が clone したとき実体の無いリンクになるため)

2 で llmtpl.conf.claude/ の中ではなくルートに置くのは、そこがターゲット(プロジェクトルート)の目印だからです。

やめたくなったら、2 手で完全に戻れる

「やめるときに困る」で始めた記事なので、この仕組み自体をやめる手順も書いておきます。

  1. llmtpl.conf のフラグを全部 false にして llmtpl apply → symlink が全部外れ、生成物の本文がプロジェクト固有の内容だけに戻る(1 行目の生成マーカはまだ残っています)
  2. mv .claude/CLAUDE.md.tmpl .claude/CLAUDE.md → 原本で生成物ごと上書きして、生成マーカも消える(.tmplrm するだけだと、存在しない原本を指すマーカが残ります)

あとは llmtpl.conf.gitignore の数行を消すだけです。手作業で消して回るところはありません。 戻る先が「1 手前」ではなく「最初の手書き構成」なのは、.tmpl が原本のままで、生成物側に一度も手を入れていないからです。


覚える言葉は 2 つ、配られ方は 4 通り

バンドル<置き場>/<フラグ名>/ のディレクトリ。機能 1 個ぶんのかたまり。中身はプロジェクトの中と同じ形にします。

ターゲット — 設定を受け取るプロジェクトルート。目印は llmtpl.conf を持つこと、それだけです。生成物も symlink も、この配下に出ます。1 つのリポジトリに複数あって構いません。

バンドルに置けるものと、配られ方はこの 4 通りです。

バンドルに置く ターゲット側でどうなるか
.claude/CLAUDE.md.tmpl .claude/CLAUDE.md へ差し込まれる
.claude/settings.json.tmpl .claude/settings.json へ deep merge される
.claude/rules/ .claude/rules/<フラグ名>/ として ディレクトリごと symlink
.claude/skills/ agents/ commands/ hooks/ 中のエントリごとに symlink

.claude を特別扱いしていないので、.cursor/.github/ を作れば同じ規則でそこへ配られます。

バンドルを直したら llmtpl apply を叩き直します。その叩き忘れを検出する check(CI 向け)、差し込む位置を選ぶ受け口、全リポジトリ共通の既定フラグ、バンドル置き場の場所を明示する bundle_root は README にあります。

https://github.com/ryokwkm/llmtpl


おわりに — 自分のツールが防ぐと言っていた事故を、自分で踏んでいた

公開の前に全体を見直したら、こんなものが出てきました。

バンドルに置いた .tmpl の中身は、受け取る側の同じ位置に同じ名前の .tmpl が無いと、どこにも入りません。 .claude/CLAUDE.md.tmpl を持たないリポジトリでは、settings.json と symlink は配られるのに、CLAUDE.md の指示だけが届かない。しかも当時は警告も出さず、終了コードも 0 でした。

まさに冒頭に書いた「③④ だけ残る」状態を、防ぐと言っているツールが自分で作っていたわけです。しかも自分のリポジトリの 1 つで現に踏んでいて、hook は毎セッション動いているのに、その作法を書いた 6KB の規約だけが AI に届いていませんでした。

とどめに、README には**「apply が知らせます」と書いてありました**。実装を見たら、その処理は存在しませんでした。書いたつもりになっていたやつです。

いまは 1 行出ます。

⚠️  logcheck の .claude/CLAUDE.md.tmpl が届いていません(このターゲットに .claude/CLAUDE.md.tmpl が無いため。作れば入ります)

種類ごとの置き場に散らばったままだと、この手の欠けは目視では見つかりません。1 か所に畳んでおくと、道具のほうが数えてくれます。

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?