Claude Codeのhookを書きやすくするcchookを作った

背景

  • Claude CodeはCLAUDE.mdに書いていたとしても、結構忘れがちです
    • 毎回Claude Codeに自分で指摘するのは疲れます...
  • 条件をトリガーに何かを必ず実行させる仕組み、hookがClaude Codeにはあります
  • hookのinputはjsonが渡ってくるので、jqなどで加工すればokかつ簡単に書けるので便利ではある
  • しかし、hookは簡単に読みにくいものになってしまう
    • 例えばStopのhook(処理が終わったらntfy経由で通知させる)だと以下のようなめっちゃ長いものが簡単にできてしまう
    • 入力のjsonの複数の要素を使い回したい場合、一度tmpファイルとして出力しないといけない
    • jqのフィルタを複数回書かないといけない
    • jsonの中の文字列なので、yamlのように適度に改行しながら記述することができない
{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "transcript_path=$(jq -r '.transcript_path') && cat \"${transcript_path}\" | jq -s 'reverse | map(select(.type == \"assistant\" and .message.content[0].type == \"text\")) | .[0].message.content[0]' > /tmp/cc_ntfy.json && ntfy publish --markdown --title 'Claude Code' \"$(cat /tmp/cc_ntfy.json | jq -r '.text')\""
          }
        ]
      }
    ]
  },
}

解決策: cchookを作った

さすがにしんどいなと思ったので、hookをもっと簡単に書けるツールを自作しました。

以下のような感じで読みやすく、かつjq likeなテンプレートを使って制御できます。「ファイルの拡張子が.goで」とか「このファイルが存在したら」といった頻出の表現はconditionsでさっと書けるようにしているので、何の設定かが一目で分かるようにしてます。

Stop:
  - actions:
      - type: command
        command: >
          MESSAGE=$(cat '{.transcript_path}' | 
            jq -rs 'reverse | map(select(.type == "assistant" and .message.content[0].type == "text")) | .[0].message.content[0].text')
          ntfy publish --markdown --title 'Claude Code Complete' "$MESSAGE"

PreToolUse:
  - matcher: "Bash"
    conditions:
      - type: command_starts_with
        value: "python"
    actions:
      - type: output
        message: "pythonは使わず`uv`を代わりに使いましょう"
  - matcher: "WebFetch"
    conditions:
      - type: url_starts_with
        value: "https://github.com"
    actions:
      - type: output
        message: "WebFetchではなく、`gh`コマンド経由で情報を取得しましょう"

PostToolUse:
  - matcher: "Write|Edit|MultiEdit"
    conditions:
      - type: file_extension
        value: ".go"
    actions:
      - type: command
        command: "gofmt -w {.tool_input.file_path}"
  - matcher: "Write|Edit|MultiEdit"
    conditions:
      - type: file_exists
        value: ".pre-commit-config.yaml"
    actions:
      - type: command
        command: "pre-commit run --files {.tool_input.file_path}"

さっそく使ってみてるけど、普通に便利なので是非使ってみてね。