3行で
- Claude Code などのエージェントに読ませる skill / command / agent を、YAML 1枚で宣言的に管理する CLI を作りました
kata.ymlとkata.lockを git に入れておけば、別のマシンではkata sync一発で同じ環境が復元されます- Go 製の単一バイナリです → https://github.com/cutmail/kata
~/.claude が秘伝のタレになっていく問題
エージェントに渡すスキルやコマンドは、気づくと ~/.claude/skills/ の下に手作業で溜まっていきます。
GitHub で見つけたスキルを clone してコピーし、自分用に少し書き換え、別のリポジトリから持ってきたコマンドを置き、そのうちどれがどこから来たものか分からなくなる。パッケージマネージャーが登場する前の /usr/local と同じ状態です。
これの何が困るかというと、マシンを移ったときに再現できないことです。新しい PC をセットアップするとき、あるいは仕事用と個人用で環境を揃えたいとき、結局ディレクトリごとコピーすることになります。しかもコピーした先では、元がどのリポジトリのどのコミットだったのか分からないので、上流の更新を取り込むこともできません。
やりたかったのは単純で、~/.claude を「溜まっていく場所」ではなく「宣言する対象」にすることでした。
作ったもの
kata という CLI を作りました。
$ kata sync + my-review skill ~/.claude/skills/my-review + pdf skill ~/.claude/skills/pdf 2 created, 0 updated, 0 removed, 0 unchanged
管理用のリポジトリに置くのは、この3つだけです。
my-agent-config/ ├── kata.yml # 何をどこから持ってくるかの宣言 ├── kata.lock # 解決済みのコミット └── local/ # 自作スキルの実体
これを git に入れておけば、別のマシンでは clone して kata sync するだけで済みます。
使ってみる
インストール
macOS なら Homebrew で入ります。
$ brew install --cask cutmail/tap/kata
GitHub Releases にバイナリも置いてあります(macOS / Linux / Windows、amd64・arm64)。go install でも入ります。
$ go install github.com/cutmail/kata/cmd/kata@latest
セットアップ
管理用のリポジトリを作って kata init します。
$ mkdir my-agent-config && cd my-agent-config $ git init && kata init created kata.yml put your own skills under local/ and register them with 'kata add'
公開リポジトリのスキルを取り込みます。リポジトリ内のサブディレクトリを指定できるので、スキル集から必要なものだけを持ってこられます。
$ kata add anthropics/skills --path skills/pdf --ref main added pdf (skill) to kata.yml + pdf skill ~/.claude/skills/pdf 1 created, 0 updated, 0 removed, 0 unchanged
自作のスキルは local/ に置いて登録します。実体が同じリポジトリに入るので、編集するとそのまま git の差分になります。
$ mkdir -p local/skills/my-review && vim local/skills/my-review/SKILL.md $ kata add ./local/skills/my-review added my-review (skill) to kata.yml + my-review skill ~/.claude/skills/my-review 1 created, 0 updated, 0 removed, 0 unchanged
skill / command に加えて agent(サブエージェント定義)も同じ仕組みで管理できます。.md ファイルは type を省略すると command と解釈されるので、agent だけは --type agent を明示します。
既に育ってしまった ~/.claude を取り込む
新規に始める分にはこれでいいのですが、実際に困っていたのはむしろ逆で、すでに手作業で育ってしまった ~/.claude をどう卒業するかでした。そこで kata import を用意しました。
$ kata import --dry-run dry run: nothing was changed + my-review skill ~/.claude/skills/my-review -> ./local/skills/my-review + pr command ~/.claude/commands/pr.md -> ./local/commands/pr.md skip pdf managed by kata (declared in ~/dotfiles) 2 to import, 1 skipped
既定では local/ へコピーしてマニフェストに書くだけで、~/.claude 側には一切触れません。元のファイルとコピーが両方存在する状態で確認してから、--adopt を付けたときだけ元を退避して symlink に差し替えます。「取り込んでみたら壊れた」が起きないように、実体を動かす操作は明示的なオプトインにしてあります。
状態の確認
kata list で今の状態が分かります。
$ kata list NAME TYPE STATUS PROFILES SOURCE DEST pdf skill linked all git+https://github.com/anthropics/skills@f17010c ~/.claude/skills/pdf my-review skill linked all local:./local/skills/my-review ~/.claude/skills/my-review
STATUS は linked / copied / missing / drifted / broken / orphan のいずれかになるので、マシンが宣言とズレていないか一目で分かります。CI で使うなら kata status の方が向いています。ズレている項目だけを出力し、何かあれば終了コード 1 を返すので、そのままパイプラインに組み込めます。
あとは commit して push するだけです。
$ git add -A && git commit -m "my agent setup" && git push
別のマシンで
clone して kata sync します。これだけです。
$ git clone <repo> && cd <repo> $ kata sync + my-review skill ~/.claude/skills/my-review + pdf skill ~/.claude/skills/pdf 2 created, 0 updated, 0 removed, 0 unchanged
kata.lock にコミットハッシュが記録されているので、ref: main のような動く参照を書いていても、復元されるのは元のマシンと同じツリーです。
その他のコマンド
kata update [name...]— 動く ref を再解決してロックを進める(lock はsyncでは勝手に動かない、後述)kata doctor— マニフェストとロックの食い違いなど、環境がおかしい理由を説明するkata remove <name>— マニフェストから消して撤去する(kata.ymlを直接編集してsyncするのと結果は同じ)kata prune— もうどこからも参照されていないキャッシュを掃除する。--applyを付けない限り何も消えない
設計で考えたこと
宣言的にする
コマンドは意図的に sync 一本に寄せました。install と uninstall を別々に持つのではなく、「マニフェストがあるべき状態、sync はそこへ収束させる操作」という Terraform 的なモデルにしています。
なので kata.yml からパッケージを消して sync すれば、配置も自動で撤去されます。同じ sync を何度実行しても結果は変わりません。
$ kata sync 0 created, 0 updated, 0 removed, 2 unchanged
lock でコミットを固定する
ref: main と書いたときに、マシンごとに違うものが降ってくるのでは意味がありません。npm や Bundler と同じで、解決結果を kata.lock に固定します。
resolved: - name: pdf type: skill source: git+https://github.com/anthropics/skills ref: main commit: f17010c9bb483898c1d9c9f42dde2b3a98889434 subpath: skills/pdf
sync はロックを正として動くので、上流が進んでも勝手には追随しません。更新したいときだけ kata update を叩く、明示的な操作にしています。
地味に気を使ったのが --profile で絞り込んだときの挙動です。kata sync --profile work で対象を work プロファイルだけに絞っても、選外になったパッケージのロックは消しません。マニフェストに宣言が残っている限りロックも残す、というルールにしないと、「たまたま今回 --profile を付け忘れた」だけで別マシンの再現性が壊れてしまうからです。
symlink が使えない環境にも対応する
配置はデフォルトでは symlink です。一時リンクを作ってから rename で差し替えるので、切り替えは常にアトミックになり、中途半端な状態が残りません。
~/.claude/skills/pdf -> ~/.kata/store/repos/82a901da1b9b-f17010c9bb48/skills/pdf ~/.claude/skills/my-review -> ~/my-agent-config/local/skills/my-review
ただ symlink が作れない、あるいは作りたくない環境もあります。そのために strategy: copy(実体をコピーする)と strategy: auto(symlink が使える環境では link、使えなければ copy に自動で倒す)を用意しました。scope: project と strategy: copy を組み合わせると、配置先の .claude/ をそのままコミットできるので、チームでスキルを共有したいときはこちらが向いています。
copy 戦略は symlink と違って「誰が置いたか」が実体に書かれていないので、state.json に配置時点の内容ダイジェストを記録し、撤去や上書きの前にそれと照合します。一致しなければ「配置後に誰かが編集した」とみなして警告を出し、--force を付けても削除ではなく退避に留めます(壊さない、という不変条件は戦略が変わっても揺らがないようにしてあります)。
CI に windows-latest を含めているのはこの copy/auto 経路を実際に踏むためです。実際、Windows の絶対パス(C:\...)を URL パーサがドライブレターとスキームを取り違えたり、symlink の差し替えで使う MoveFileEx がディレクトリの symlink を置換できなかったりと、macOS や Linux の手元環境だけでは絶対に気づけない不具合を CI で何度か拾いました。
取得元は信用しない
url 取得元は tar.gz / tgz / zip を展開します。ここは攻撃対象になり得る箇所なので、書き込む前に各エントリの展開後パスが展開先ディレクトリの内側に収まっているかを確認し、symlink や特殊ファイルのエントリは拒否しています。サイズの上限判定にはアーカイブが自己申告するサイズを使わず、実際に読み取ったバイト数を見るようにしました。マニフェストの sha256 とロックのダイジェストの照合も、展開の前に行います。
実装
Go で書きました。直接の依存は cobra、yaml.v3、go-git、golang.org/x/sys の4つです。x/sys は state.json を書き換えるときの排他ロック(Unix は flock、Windows は等価な API)にだけ使っています。純 Go 中心の構成なので cgo なしでクロスコンパイルでき、配布は単一バイナリで済みます。
構成は、拡張点になる2つのインターフェースを軸にしています。
cmd/kata/ CLI (cobra) internal/manifest kata.yml のパースと検証 internal/lockfile kata.lock internal/state 配置実績の記録 (state.json) internal/store 取得物のキャッシュ internal/source 取得元とアーカイブの展開 (git, url, local) ← Fetcher internal/target 配置先 (Claude Code) ← Resolver internal/linker 配置と撤去 (symlink / コピー) internal/digest 内容ダイジェスト。所有の判定と取得物の検証に使う internal/copyfs ディレクトリ木の複製。import が local/ へ取り込むのに使う internal/app 全体のオーケストレーション
取得元を増やすなら source.Fetcher を、Cursor など他のエージェントに対応するなら target.Resolver を実装すれば足りるようにしてあります。
~/.kata/store は URL とコミットをキーにした純粋なキャッシュで、消しても sync で復元されます。「消しても壊れないディレクトリ」と「消すと困るディレクトリ」を明確に分けておくと、後から挙動を説明しやすくなって良かったです。
リリースは GoReleaser に任せています。タグを push すると各プラットフォーム分のアーカイブが GitHub Releases に上がり、Homebrew の tap も更新されます。アーカイブには GitHub ネイティブの SLSA build provenance attestation を付けているので、gh attestation verify でタグの付いたコミットからビルドされたことを検証できます。
名前について
kata は武道の「型」です。
再現可能な所作の集合、という意味がそのまま「スキルを型として登録し、どのマシンでも同じ型を再現する」に重なるので気に入っています。短くて打ちやすいのも良いところです。
今後
現時点で skill / command / agent の3種別、git・URL アーカイブ・ローカルの3取得元、symlink / copy / auto の配置戦略、user / project の2スコープ、profiles による絞り込みまでは一通り揃いました。
残っているのはこのあたりです。
- MCP サーバー設定のマージ
- Claude Code 以外の Resolver 実装(
target.Resolverを実装すれば追加できる想定です) kata.ymlの JSON Schema 公開
機能が増えるほど「kata が作っていないものには触らない」という一番の不変条件を壊しやすくなるので、そこは今後も最優先で守っていくつもりです。
おわりに
エージェントに渡す設定は、これから確実に増えていくものだと思っています。増えていくものは、そのうち必ず「どう管理するか」が問題になります。
同じ問題意識を持っている方がいたら、ぜひ触ってみてください。Issue や PR も歓迎です。









