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になります。
取るときのコツは三つです。
- 取得前にPreserve logを有効にします。ページ遷移をまたいでも記録が消えません。
- 画面の操作は、知りたい機能に絞って行います。ログインや無関係なページ遷移は含めません。
- 一覧の「次のページ」や絞り込みも、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の形・パラメータ名・ヘッダー名・応答のキー名までに限り、値は人の手元に残します。その一手間で、クライアントは壊れても検知できる形で書け、秘密情報は会話に出ません。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。