Claude Media
Claude CodeでHARから裏のJSON APIを洗い出す手順

Claude CodeでHARから裏のJSON APIを洗い出す手順

画面が裏で呼ぶJSON APIをHARから探し、公開APIの有無を確かめてからクライアントにする流れです。Claudeに渡すのはURLの形とキー名だけで、値は渡しません。

社内で使っているWeb画面に、CSVの書き出しボタンも公開APIも見当たらない。それでも一覧のデータは画面に出ている以上、ブラウザは裏でどこかのJSON APIを呼んでいます。

そのAPIを突き止めてクライアントにするとき、順番を間違えると二つの事故につながります。公開APIがあるのに非公開の経路を使ってしまうこと。そして、HARに残ったCookieやトークンをそのままモデルに渡してしまうことです。

この記事では、HARから呼び出しの形だけを抜き出し、それをClaude Codeに渡してクライアントの下書きを作る流れを示します。値は一切渡さない構成にしています。

最初にやるのは公開APIと規約の確認

HARを開く前に、公開APIがあるかを調べます。ここを飛ばすと、作ったクライアントがサービス側の仕様変更で突然壊れる経路を選ぶことになります。

確認する場所は次の四つです。

  • サービスのドキュメントやヘルプにある「API」「開発者向け」のページ
  • 管理画面にあるAPIキーの発行画面や、連携(Webhook・CSV出力)の設定
  • 利用規約と、自動アクセスやスクレイピングに触れた条項
  • robots.txt と、同じドメインにある openapi.json などの公開仕様

規約に自動アクセスを禁じる条項があれば、HARから見つけたAPIでクライアントを作る話はここで止まります。許可の有無はサービスごとに決まっていて、このページの記述から一般論は引けません。契約している業務システムなら、サポート窓口に「データを機械で取り出す正規の方法」を尋ねるほうが早い場合もあります。

公開APIが見つかったら、そちらを使います。OpenAPI仕様が手に入るなら、openapi-typescriptで型を作る手順に進めます。以降は、公開APIがなく、規約上も問題ないと判断できた場合の話です。

HARを取る — 絞って取り、Fetch/XHRだけ見る

ChromeのDevToolsのNetworkパネルで操作を一通り行い、HARに書き出します。

ドキュメントによると、画面上部のアクションバーにあるExport HAR (sanitized)...から書き出せます。リクエスト表を右クリックしてCopyを開き、Save all [listed] as HAR (sanitized)を選ぶ方法もあります。後者は「listed」とあるとおり、表に出ているものだけが対象です。タイプ絞り込みでFetch/XHRだけを表示してから書き出せば、画像やCSSを含まない小さなHARになります。

取るときのコツは三つです。

  1. 取得前にPreserve logを有効にします。ページ遷移をまたいでも記録が消えません。
  2. 画面の操作は、知りたい機能に絞って行います。ログインや無関係なページ遷移は含めません。
  3. 一覧の「次のページ」や絞り込みも、1回ずつ操作します。パラメータの意味が後で読み取れます。

「sanitized」の書き出しは、Cookie、Set-Cookie、Authorizationのヘッダーを除きます。ただしURLのクエリ文字列や本文までは同じ扱いとは書かれていません。顧客のHARを調べる場合の消毒スクリプトはHARファイルの解析手順にあるので、そちらも参照してください。自分の画面を自分で取る今回は、「値は渡さない」という別の方針で守ります。

また、HARの画面上の見え方を確かめたいときは、DevToolsにImport HARがあります。ファイルをNetworkパネルにドラッグしても読み込めます。Claudeに渡す前に、人の目で一度流れを確認する用途に向きます。

値を捨てて構造だけを抜き出す

HARはJSONで、log.entries の各要素が1リクエストです。仕様上、リクエストには method、url、headers、queryString、postData があり、レスポンスの content には mimeType と、あれば text が入ります。

ここで考え方を変えます。クライアントの設計に必要なのは「どのURLに、どのパラメータ名で、どのヘッダー名を付けて、何が返るか」です。トークンの値やレスポンスの中身は要りません。そこで名前だけを残して値を捨てるjqを用意します。

# shape.jq — レスポンスがJSONのリクエストだけを、形にまとめる
def keys_of: if type=="object" then keys
  elif type=="array" and length>0 then [.[0]|keys_of]
  else type end;
[.log.entries[]
 | select(.response.content.mimeType | test("json"))
 | {method: .request.method,
    path: (.request.url | sub("^https?://[^/]+";"") | sub("\\?.*";"")),
    host: (.request.url | capture("^https?://(?<h>[^/]+)").h),
    params: [.request.queryString[].name],
    headers: [.request.headers[].name],
    body_keys: (try (.request.postData.text | fromjson | keys) catch []),
    status: .response.status,
    response_keys: (try (.response.content.text | fromjson | keys_of) catch null)}]
| group_by([.method,.host,.path])
| map({method:.[0].method, host:.[0].host, path:.[0].path,
       calls:length,
       params:(map(.params)|add|unique),
       headers:(map(.headers)|add|unique),
       body_keys:(map(.body_keys)|add|unique),
       statuses:(map(.status)|unique),
       response_keys:(map(.response_keys)|unique)})

URLの ? 以降は落とし、パラメータは名前だけを残します。リクエスト本文もキー名だけ、レスポンスもトップレベルのキー名だけです。同じURLの呼び出しは1行にまとまります。

5件のリクエスト(HTML、JS、JSON3件)を入れた架空のHARで実行すると、次の2行になりました。HTMLとJSはJSON判定で外れ、orders を2回呼んだ分は1行に束ねられています。

jq -f shape.jq app.har | jq -c '.[]' > shape.jsonl
cat shape.jsonl
{"method":"GET","host":"app.example.com","path":"/api/v2/orders","calls":2,"params":["page","status"],"headers":["Accept","Authorization","X-CSRF-Token"],"body_keys":[],"statuses":[200],"response_keys":[["items","next"]]}
{"method":"POST","host":"app.example.com","path":"/api/v2/orders/search","calls":1,"params":[],"headers":["Content-Type"],"body_keys":["q"],"statuses":[200],"response_keys":[["items"]]}

ここから読み取れるのは、/api/v2/orders が status と page を受け取ること、next というキーでページを辿ること、そして認証に Authorization と X-CSRF-Token のヘッダーが使われていることです。ヘッダーの値はどこにも出ていません。

この方法が拾えないもの

名前だけに絞っても、漏れる値が残る場合があります。

  • /orders/12345 のようにURLのパスにIDや氏名が入る場合。パスはそのまま残るので、同じ形ごとに束ねたいときは数字を :id に置き換える一手間が要ります。
  • キー名そのものが情報になる場合。顧客名がキーになっているJSONなどです。
  • mimeType が text/plain などでJSON判定から外れるAPI。select の条件を見直します。
  • GraphQLのように、URLが1本で本文の query が中身を決める場合。body_keys だけでは足りません。

渡す前にshape.jsonlを人が一度読み、固有名や秘密らしき文字列がないかを確かめてください。

生のHARをClaude Codeから遠ざける

Claude Codeに渡すのは shape.jsonl だけにします。生のHARは raw/ に置き、読み取りを拒否するルールを .claude/settings.json に書きます。

{
  "permissions": {
    "deny": ["Read(./raw/**)"]
  }
}

permissionsのドキュメントによると、ルールの評価順はdeny、ask、allowで、最初に一致したものが結果を決めます。denyに入れたパスは、後から別のallowでは開けません。

ただし限界もあります。Readのdenyは、ファイルを名前で指さずに読むコマンド(grep -r pattern . など)や、PythonやNodeのスクリプトが自分で開くファイルには効きません。生のHARに対するjqの実行は人が行い、Claude Codeには shape.jsonl だけが見える状態を保ちます。OSの段階で止めたい場合は、同じドキュメントが案内するサンドボックスを有効にします。

shape.jsonlからクライアントの下書きを作らせる

渡す先がshape.jsonlだけなら、標準入力に流し込む形が使えます。非対話モードの claude -p は標準入力を読み、ドキュメントでは、diffを流し込む例について「Bashの権限が要らない」と説明されています。ファイルを読むツールを許可しなくて済みます。

まず、エンドポイントの台帳を構造化出力で作らせます。

cat shape.jsonl | claude -p \
  "各行は画面が呼ぶJSON APIの形です。値は含まれません。\
エンドポイント台帳を作ってください。認証ヘッダーの値は推測せず、\
環境変数名で参照する前提にしてください。" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"endpoints":{"type":"array","items":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"},"params":{"type":"array","items":{"type":"string"}},"pagination":{"type":"string"},"auth_headers":{"type":"array","items":{"type":"string"}}},"required":["method","path"]}}},"required":["endpoints"]}' \
  | jq '.structured_output'

--json-schema を付けると、スキーマに沿った結果が structured_output に入ります。ドキュメントには、有効な構造化出力が得られなかった実行ではこのフィールドが付かないと書かれています。スクリプトで使うときは、フィールドの有無を先に確かめてください。標準入力の上限は10MBです。shape.jsonlはHAR本体よりずっと小さくなるので、通常は収まります。

例えば次のような台帳になれば、狙いどおりです(出力形式の例であり、実際の返答ではありません)。

{"endpoints":[{"method":"GET","path":"/api/v2/orders",
  "params":["status","page"],"pagination":"next",
  "auth_headers":["Authorization","X-CSRF-Token"]}]}

台帳ができたら、クライアントの下書きを作らせます。ここでもルールを CLAUDE.md に置くと、毎回の指示を短くできます。

## 非公開APIクライアントの書き方
- 認証情報はコードに書かず、環境変数 `APP_TOKEN` から読む
- 呼び出しは1秒に1回以内。429や5xxは指数バックオフで3回まで
- 応答のキーが想定と違えば例外にして止める(黙って空を返さない)
- 実サーバーへの接続テストは書かない。応答はfixtureで固定する

最後の二つが大事です。非公開APIは予告なく形が変わるので、キー欠落を例外にして気づけるようにします。実サーバーにつながないテストなら、BeautifulSoupのスクレイパーをfixtureで試験した手順と同じ型で回せます。画面操作のほうが安定する場合は、Playwrightでスクレイピングを組む選択肢と比べてください。

認証は人が最後に足す

HARから見えたのは、認証に使われるヘッダーの名前までです。値はshape.jsonlにないので、Claudeが書くクライアントは os.environ["APP_TOKEN"] を読むだけの形になります。

トークンの取り方は、サービスの認証方式次第です。短い期限のセッショントークンをブラウザから取り出す運用は、期限切れのたびに手作業が要ります。APIキーの発行画面があるなら、それを使うほうが運用は軽くなります。いずれの場合も、トークンは .env などコードの外に置き、リポジトリに含めません。

つまずきやすい点

落とし穴

shape.jqが空を返す

mimeType の判定が合っていない場合が多いです。jq -r '.log.entries[].response.content.mimeType' app.har | sort | uniq -c で実際の値を見て、select の条件を直します。

落とし穴

同じURLがIDごとに別行になる

パスにIDが入ると、group_by は別のエンドポイントとして数えます。jqの sub("/[0-9]+";"/:id") を path に足して束ねてください。

  • 一覧の「次のページ」を押さずにHARを取ると、page や cursor のパラメータが1回も現れず、ページ送りの仕組みが台帳に載りません。
  • HARの response.content.text はテキストだけが入る仕様で、取得時にDevToolsが本文を保持していないと空になります。レスポンスの形が取れていない行は、ブラウザで再度操作して取り直します。

よくある質問

公開APIがなければ、HARで見つけたAPIを使ってよいですか。

使ってよいかどうかは、サービスの利用規約と契約で決まります。このページから判断はできません。規約が自動アクセスを禁じていれば、作らないのが前提です。迷うときは、サービスの窓口に正規のデータ取得手段を尋ねてください。

画面の操作ごとに叩かれるAPIが多すぎて絞れません。

Networkパネルの検索(Command + F / Control + F)で、画面に表示されている値を検索すると、その値を返したリクエストが黄色で強調されます。見つけたリクエストから逆に辿ると、必要なエンドポイントに早く行き着きます。

まとめ

非公開APIは、見つけるより先に、使ってよいかを確かめるものです。確認が済んだ場合でも、Claudeに渡すのはURLの形・パラメータ名・ヘッダー名・応答のキー名までに限り、値は人の手元に残します。その一手間で、クライアントは壊れても検知できる形で書け、秘密情報は会話に出ません。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. developer.chrome.com/docs/devtools/network/reference
  2. w3c.github.io/web-performance/specs/HAR/Overview.html
  3. code.claude.com/docs/en/headless
  4. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn