📑

Claude Code Hooks × markitdown で非MDファイルを透過的にMarkdown変換する

に公開

TL;DR

Claude Code の PreToolUse Hook と Microsoft の markitdown を組み合わせて、PDF・PPTX・XLSX などの非Markdownファイルを Read 時に自動変換する仕組みを構築した。LLM側は何も意識せず、普通に Read するだけで Markdown 化された内容が読める。

背景と課題

Obsidian Vault をナレッジベースとして運用する中で、raw/ フォルダにはさまざまな形式のファイルが集まる。社内の方針資料(PPTX)、営業計画(XLSX)、規程文書(PDF)、企画書(HTML)など、Markdown 以外のファイルが大半だ。

これらを LLM Wiki に取り込む(ingest する)には、毎回手動で変換するか、スキル側に個別の変換ロジックを書く必要があった。

理想: raw/ にファイルを投入 → /wiki コマンド一発で Wiki 化。変換は裏で勝手にやってほしい。

解決策: PreToolUse Hook による透過変換

アーキテクチャ

Claude が Read(raw/foo.pptx) を実行
  ↓ PreToolUse Hook が発火
  ↓ markitdown で foo.pptx → foo.pptx.md に変換(キャッシュあればスキップ)
  ↓ updatedInput で file_path を foo.pptx.md に書き換え
  ↓ Claude は透過的に Markdown 版を読む

ポイントは updatedInput の仕組み。PreToolUse Hook は stdout に以下の JSON を出力することで、ツールの入力パラメータを書き換えられる。

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "file_path": "raw/foo.pptx.md"
    }
  }
}

これにより、Claude 本体もスキル側も変換処理の存在を意識する必要がない。完全に透過的に動作する。

Hook スクリプトの要点

# 対象判定: raw/ 配下 かつ 変換可能な拡張子
CONVERTIBLE_EXTENSIONS = {".pdf", ".pptx", ".xlsx", ".xls", ".docx", ".html", ".htm"}

# キャッシュ: 変換済みファイルが元ファイルより新しければスキップ
def is_cache_valid(source_path, converted_path):
    return os.path.getmtime(converted_path) >= os.path.getmtime(source_path)

# 変換: markitdown CLI を subprocess で実行
subprocess.run(["markitdown", source_path, "-o", output_path], timeout=120)

# 出力: tool_input の file_path のみ差し替え、他のパラメータはそのまま保持
updated_input = dict(tool_input)
updated_input["file_path"] = converted_path

Windows 環境でのハマりポイント

日本語ファイル名を含む JSON が stdin 経由で渡されるとき、Windows のデフォルトエンコーディング(cp932)では文字化けする。settings.json のコマンド定義で PYTHONUTF8=1 を付ける必要がある。

{
  "matcher": "Read",
  "hooks": [{
    "type": "command",
    "command": "PYTHONUTF8=1 python .claude/hooks/raw_markitdown_converter.py"
  }]
}

Wiki スキルとの連携

/wiki ingest コマンドは内部で Read ツールを使ってファイルを読み込むため、Hook による変換が自動的に適用される。スキル側の修正は最小限で済んだ。

  • 対象拡張子の追加: .pptx, .xlsx, .docx 等を検出対象に追加
  • キャッシュファイルの除外: *.pdf.md のような変換済みキャッシュを未処理ファイルと誤検出しないよう除外ルールを追加

対応フォーマットと検証結果

形式 ファイル例 結果
PDF 公文書.pdf(147ページ)
PPTX 本部方針.pptx
XLSX 営業計画.xlsx
HTML 企画書.html

日本語ファイル名、全角括弧(【】)、丸数字(①⑤⑨)、ネストされたサブフォルダ、いずれも問題なく動作した。

まとめ

  • markitdown: pip install markitdown[all] で PDF/Office/HTML を一括対応
  • PreToolUse Hook + updatedInput: ツール入力の透過的な書き換えで、変換処理を完全に隠蔽
  • キャッシュ: mtime 比較でシンプルに再変換を回避
  • PYTHONUTF8=1: Windows × 日本語ファイル名の落とし穴

Claude Code の Hook は「ブロックする」だけでなく「入力を書き換える」こともできる。この仕組みを使えば、ファイル形式の差異を吸収するアダプター層を手軽に構築できる。

実際の実装したときのプロンプト

https://github.com/microsoft/markitdown をインストールして
\raw に突っ込んだファイルが結構MDじゃないのがあるので、それらを読み込むときにHooksで自動的にmarkitdownを起動してほしいんだけど、そういうようにできる?
ちなみにraw/にファイルを突っ込んだら未処理のモノに対して wikiコマンドを使う感じだけど、そこともシームレスに連携される感じでよい?wikiコマンドも更新しておいた方が良ければしておいてほしい。

Discussion