お雑煮研究会

好きな焼肉は魚

気に入った Claude Code Hooks を紹介する

Claude Code の Hooks をそれなりに設定しているのですが、結構体験が変わってお得だったので自分が実際に常用している Hook の小ネタをいくつか紹介します。

この記事は Claude Code の Hooks をすでに触ったことがあり、自分で Hook を書くことに抵抗がない人向けです。

また、コード例では自作の TypeScript wrapper(cc-hooks-ts)を使っています。

sushichan044.hateblo.jp

Claude Code Hooks の公式ドキュメントはこちら。

code.claude.com

Plan 結果をプロジェクト内に出力する

Claude Code の Plan 結果はデフォルトでは ~/.claude/plans に出力されます。

私は Claude Code と OpenAI Codex の間で Plan をやり取りしたり、Plan をそのままプロジェクトの履歴として残したかったりするため、プロジェクト内に自動で書き出されるほうが都合が良いと感じています。

しかし 2025.12.26 現在、このような機能は実装されていません。

github.com

Claude Code には ExitPlanMode という Hook Event があるため、ここで Plan の内容を適当なファイルに書き出せばプロジェクト内に結果を保存できます。

Plan を整形して ${cwd}/.claude/plans 以下に保存する Hook の例:

import { defineHook } from "cc-hooks-ts";
import { format } from "oxfmt";
import path from "path";

async function dumpPlan(plan: string): Promise<string> {
  const filepath = path.join(process.cwd(), ".claude", "plans", `plan-${Date.now()}.md`);
  try {
    await Bun.write(filepath, plan);
  } catch (error) {
    return `Failed to write plan to memo note at ${filepath}. Error: ${String(error)}`;
  }

  return `Plan dumped to note file at ${filepath}`;
}

async function formatPlan(plan: string): Promise<string> {
  try {
    const pretty = await format("dummy.md", plan);
    return pretty.code;
  } catch {
    return plan;
  }
}

const hook = defineHook({
  trigger: {
    PreToolUse: {
      ExitPlanMode: true,
    },
  },

  run: async (c) => {
    const plan = c.input.tool_input["plan"] as string;
    const prettyPlan = await formatPlan(plan);
    const message = await dumpPlan(prettyPlan);

    return c.success({ messageForUser: message });
  },
});

if (import.meta.main) {
  const { runHook } = await import("cc-hooks-ts");
  await runHook(hook);
}

起動時に Plugin Marketplace をバックグラウンドで更新する

最近の Claude Code では、Subagent や Skill を Plugin として配布できるようになり、 資産の再利用がかなり楽になりました。

code.claude.com

azukiazusa.dev

一方で、Plugin Marketplace の更新はこのように手動で行う必要があります。

  • TUI の奥深くのメニューから更新する
  • claude plugin marketplace update を実行する

覚えておくのは面倒なので、 SessionStart Event に Hook を設定して起動時に Marketplace をバックグラウンドで更新するようにしています。 これで 2 回目以降の起動時には最新の Marketplace が読み込まれます。

import { regex } from "arkregex";
import { defineHook } from "cc-hooks-ts";

/**
 * $ claude plugin marketplace update
 *
 * Updating 3 marketplace(s)...
 * ✔ Successfully updated 3 marketplace(s)
 */
const extractMarketplaceAmount = regex("(?<amount>[0-9]+) marketplace");

const hook = defineHook({
  trigger: {
    SessionStart: true,
  },

  run: (c) =>
    c.defer(async () => {
      // stdout will break claude code
      const result = await Bun.$`claude plugin marketplace update`.nothrow().quiet();
      const amount = extractMarketplaceAmount.exec(result.text())?.groups?.amount ?? "-1";

      return {
        event: "SessionStart",
        output: {
          systemMessage: `Updated ${amount} plugin marketplaces.`,
        },
      };
    }),
});

if (import.meta.main) {
  const { runHook } = await import("cc-hooks-ts");
  await runHook(hook);
}

※ この Hook ではドキュメント化されていない Async Hooks というしくみを使っています。 Claude Code が Hook の終了を待たずに本来の処理を継続してくれるので、バックグラウンドで特定の処理がしたい場合に便利です。

TypeScript Wrapper での実装の詳細はこちら。

github.com

URL に対して適切なツールがある場合に WebFetch を拒否する

URL を渡して情報をコンテキストに含めるよう頼むことは多々ありますが、WebFetch ツールでは URL の内容を適切に取得できないことがあります。

たとえば GitHub の Pull Request の URL を渡した場合、HTML は取得できてもレビューに必要な情報が欠落することがあります。 また、Notion のように認証が必要なサービスでは、そもそも内容を取得できません。

このように、URL によっては WebFetch よりも専用のツールを使ったほうが適切に情報を取得できるケースがあります。

GitHub の URL であれば GitHub CLIGitHub MCP を使うほうが確実ですし、 Notion の URL の場合は公式が提供する Remote MCP の利用が推奨されています。

developers.notion.com

このようなツールの使い分け方を Agent Skills として整備しておくことも考えられますが、Skill は Claude が必要に応じて参照するものなので、常に期待する使い分けがされるとは限りません。

code.claude.com

そこで、 WebFetch の実行前に URL をチェックしてより適切な手段がある場合は Claude にフィードバックするようにしています。

※ 実際のコードが大きいので抜粋して掲載します

import { defineHook } from "cc-hooks-ts";

const hook = defineHook({
  trigger: {
    PreToolUse: {
      WebFetch: true,
    },
  },

  run: (c) => {
    const urlObj = new URL(c.input.tool_input.url);
    // raw.githubusercontent.com などファイルの中身だけが返ってくる URL は許可する
    if (isRawContentURL(urlObj)) {
      return c.success();
    }

    // Notion の URL は Notion MCP で読むべき
    if (urlObj.hostname.includes("notion.so")) {
      return c.json({
        event: "PreToolUse",
        output: {
          hookSpecificOutput: {
            hookEventName: "PreToolUse",
            permissionDecision: "deny",
            permissionDecisionReason:
              "Use mcp__notion__fetch instead of web fetch for Notion URLs.",
          },
        },
      });
    }

    // たとえば https://github.com/honojs/hono/pull/4586 に対して
    // gh pr view --repo honojs/hono 4586 を提案する
    const ghResult = parseGitHubUrlToGhCommand(urlObj);
    if (ghResult) {
      return c.json({
        event: "PreToolUse",
        output: {
          hookSpecificOutput: {
            hookEventName: "PreToolUse",
            permissionDecision: "deny",
            permissionDecisionReason: [
              "Use the GitHub CLI instead.",
              "Suggested command:",
              "```bash",
              ghResult.command,
              "```",
            ].join("\n"),
          },
        },
      });
    }

    return c.success();
  },
});

if (import.meta.main) {
  const { runHook } = await import("cc-hooks-ts");
  await runHook(hook);
}

DeepWiki MCP に不要な情報を入力させない

DeepWiki は、GitHub リポジトリの内容をコンテキストに読み込んだ AI と対話できる便利なサービスです。 MCP が用意されており、Claude Code から直接利用できます。

docs.devin.ai

ただしサービスの性質上、Private Repository に関する情報は DeepWiki 側に存在しません。 そのため Private Repository について問い合わせると、無駄な待ち時間が発生するだけでなくサービス提供側に機密性のある情報を渡してしまう可能性があります。

そこで、DeepWiki MCP を呼び出す前にGitHub CLI を使って Repository が Public かどうかをチェックし、Public でない場合は呼び出しを拒否するようにしています。

import { defineHook } from "cc-hooks-ts";

const hook = defineHook({
  trigger: {
    PreToolUse: {
      mcp__deepwiki__ask_question: true,
      mcp__deepwiki__read_wiki_contents: true,
      mcp__deepwiki__read_wiki_structure: true,
    },
  },

  run: async (c) => {
    if (Bun.which("gh") == null) {
      return c.nonBlockingError("GitHub CLI (gh) is not installed.");
    }

    const repoName = c.input.tool_input.repoName;
    const repoVisibility =
      await Bun.$`gh repo view ${repoName} --json visibility --jq '.visibility'`.nothrow().quiet();

    const isPublic = repoVisibility.text().trim().toLowerCase() === "public";
    if (repoVisibility.exitCode === 0 && isPublic) {
      return c.success();
    }

    return c.json({
      event: "PreToolUse",
      output: {
        hookSpecificOutput: {
          hookEventName: "PreToolUse",
          permissionDecision: "deny",
          permissionDecisionReason: `The GitHub repository "${repoName}" is not public or does not exist.`,
        },
      },
    });
  },
});

if (import.meta.main) {
  const { runHook } = await import("cc-hooks-ts");
  await runHook(hook);
}

まとめ

Claude Code の Hooks は公式機能で手が届かない部分の補完や、Tool 呼び出しに一定の規約を敷くのに便利です。

自分好みにカスタムして便利に使っていきましょう。

おわり

おわり