diff-profile-archives について
2つの Kernel ブラウザープロファイルアーカイブを比較するトラブルシューティング用スキルです。問題に応じてブラウザー状態を調べ、正常なプロファイルと不具合のあるプロファイルの動作差を診断します。
主な機能
- 01Kernel プロファイルの取得と展開
- 02問題に関係するブラウザー状態の比較
- 03Cookie と Web ストレージの確認
- 04設定と拡張機能の確認
- 05ログインデータと履歴の確認
- 06認証情報の値を出力しない配慮
活用シーン
- 01特定プロファイルでのログイン失敗の調査
- 02プロファイル間のボット検知の比較
- 03保存済み認証情報や設定の欠落確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kernel/ skills --skill diff-profile-archivesインストール後、Claude Code で/diff-profile-archivesを入力して読み込まれたか確認します。
詳細
Diff Kernel Profile Archives
Kernel ブラウザプロファイル 2 つの実際の内容を比較して、報告された問題の原因となり得る状態の違いを特定します。このスキルでは、Kernel CLI を使って両方のプロファイルをダウンロードして展開し、動作の違いにつながる可能性が特に高い項目(Cookie、ローカル/セッションストレージ、設定、拡張機能の状態、ログインデータ、履歴)に焦点を当てた構造化差分を作成します。
Inputs
ユーザーは次の情報を指定する必要があります。
- Profile A — 「ベースライン」または「正常に動作する」プロファイル(ID または名前)
- Profile B — 「対象」または「動作しない」プロファイル(ID または名前)
- Issue description — B では発生するが A では発生しない事象(例:「chase.com のログインフローはプロファイル B では失敗するが、A では成功する」、「datadome のチャレンジが B では必ず表示されるが、A では表示されない」、「保存済みの認証情報が見つからない」)
不足している情報があれば、作業を進める前にユーザーに確認してください。問題の説明は重要です。差分でどの項目を重視するかを判断するために使用します。
Prerequisites
- Kernel CLI がインストールされ、認証済みであること(
kernel auth) KERNEL_API_KEYが設定されているか、ユーザーがkernel loginでログインしていることsqlite3とjqが使用可能であること(必要に応じてapt-get install -y sqlite3 jqでインストール)- プロファイルごとに約 500MB の空きディスク容量(プロファイルは大きくなる場合があります)
Workflow
Step 1: Set up workspace
WORKDIR=$(mktemp -d -t profile-diff-XXXXXX)
cd "$WORKDIR"
mkdir -p a b diff
echo "Workspace: $WORKDIR"
Step 2: Resolve and download both profiles
# Replace these with the profile IDs or exact names from the user.
PROFILE_A='<working-id-or-name>'
PROFILE_B='<broken-id-or-name>'
# Record metadata and confirm the identifiers resolve to two distinct profile IDs.
kernel profiles get "$PROFILE_A" -o json | tee diff/profile-a.json
kernel profiles get "$PROFILE_B" -o json | tee diff/profile-b.json
A_ID=$(jq -r '.id' diff/profile-a.json)
B_ID=$(jq -r '.id' diff/profile-b.json)
[ "$A_ID" != "$B_ID" ] || { echo "Both inputs resolve to $A_ID" >&2; exit 1; }
# --to is an extraction directory, not an archive filename.
kernel profiles download "$PROFILE_A" --to a
kernel profiles download "$PROFILE_B" --to b
プロファイル名と ID は、CLI のアクティブなプロジェクトのスコープ内で解決されます。ユーザーがプロジェクトを指定した場合は、すべての profiles get および profiles download コマンドで同じ --project <id-or-name> を使用してください。
CLI はデフォルトで tar.zst を要求し、応答をストリーミングしながら解凍して、--to の直下に展開します。アーカイブファイルは残りません。転送量を抑えるため、デフォルトのままにしてください。zstd の解凍や形式に関する問題のデバッグで解凍処理を回避する場合に限り、--format tar を使用してください。この場合も CLI は応答を --to に展開します。
kernel profiles download "$PROFILE_A" --format tar --to a
CLI がプロファイルに保存済みデータがないと報告した場合は、そこで中止してください。Kernel が状態を取得できるよう、まずブラウザーセッションでプロファイルを使用する必要があります。
Step 3: Inspect the extracted layout
各側には通常、Chrome のユーザーデータディレクトリ構成(例:Cookies、Local Storage/、Preferences などを含む Default/)があります。続行する前に、構成を把握するため find a b -maxdepth 3 -type d | sort を実行してください。
ステップ 4:問題の説明に基づいて絞り込む
ユーザーが説明した内容に基づいて、比較する対象を選びます。すべてを比較する必要はありません。Chrome のプロファイル全体には何百ものファイルが含まれており、その大半は関係ありません。次の基準で絞り込みます。
| 問題のキーワード | まず比較する対象 |
|---|---|
| ログイン、サインイン済み、セッション、「ログアウトされた」 | Cookies、Session Storage、Login Data、Local Storage |
SPA/シングルページアプリのログイン(#/... 形式のルートを含む URL) | まず Session Storage、次に Local Storage、最後に Cookies — SPA では、Cookie ではなく sessionStorage に認証状態を保存することがよくあります |
| captcha、datadome、cloudflare、akamai、challenge、ブロックされた | Cookies(ベンダー固有の名前)、Local Storage、Preferences 内のフィンガープリント関連の指標 |
| 拡張機能、プラグイン、コンテンツスクリプト | Extensions/、Local Extension Settings/、Extension State、Secure Preferences |
| 自動入力、保存済みパスワード、認証情報 | Login Data、Login Data For Account、Web Data |
| 設定、言語、タイムゾーン、UA、ビューポート | Preferences、Secure Preferences |
| 履歴、キャッシュ、「記憶している」 | History、Top Sites、Visited Links |
| ストレージ、indexeddb、quota | Local Storage、Session Storage、IndexedDB、Service Worker |
| ログイン後の初期化が完了する前に保存されたプロファイル(保存時の競合) | Session Storage(センチネルキーがない場合)、初期化のタイミングを追跡する手掛かりとしての Local Storage の分析イベント(例:Mixpanel の User Login、Segment の track) |
判断に迷ったら、Cookies + Session Storage + Local Storage + Preferences から始めます。「A では動くが B では失敗する」という問題の大半は、これらで原因を説明できます。URL ハッシュ(/#/...)でルーティングする SPA 形式のサイトでは、Session Storage を最初に確認します。タブ内の認証情報がそこに保存されていることが多く、Cookie だけでは状況を把握できません。
ステップ 5:対象を絞った差分を確認する
Cookies(SQLite)
v80 以降の Chrome では、Cookie の内容は
encrypted_valueに暗号化され、valueは空のままです。必ずlength(encrypted_value)を選択してください。length(value)はすべての行で0を返すため、実際の差異が見えなくなります。
for side in a b; do
# Cookies live under Default/Cookies (or Default/Network/Cookies on newer Chrome)
COOKIE_DB=$(find "$side" -name 'Cookies' -not -name '*-journal' | head -1)
if [ -z "$COOKIE_DB" ]; then
echo "(no Cookies DB)" > "diff/cookies-$side.txt"
continue
fi
sqlite3 "$COOKIE_DB" \
"SELECT host_key, name, length(encrypted_value), is_secure, is_httponly, samesite, expires_utc, source_scheme
FROM cookies ORDER BY host_key, name;" \
> "diff/cookies-$side.txt" 2>/dev/null || echo "(cookies query failed)" > "diff/cookies-$side.txt"
done
diff -u diff/cookies-a.txt diff/cookies-b.txt > diff/cookies.diff || true
問題に関係するホストのみを対象にします。
HOST=chase.com # adapt to the issue
for side in a b; do
sqlite3 "$(find $side -name Cookies -not -name '*-journal' | head -1)" \
"SELECT host_key, name, length(encrypted_value), expires_utc, is_secure, is_httponly
FROM cookies WHERE host_key LIKE '%$HOST%' ORDER BY host_key, name;" \
> "diff/cookies-$HOST-$side.txt"
done
diff -u "diff/cookies-$HOST-a.txt" "diff/cookies-$HOST-b.txt" || true
存在する場合に、一方にはあってもう一方にはないものとしてフラグを立てる、一般的なボット検出用 Cookie 名:_abck、bm_sz、bm_sv(Akamai);__cf_bm、cf_clearance(Cloudflare);datadome(DataDome);_px*、pxvid(HUMAN/PerimeterX);incap_ses_*、visid_incap_*、reese84(Imperva)。
Preferences(JSON)
for side in a b; do
PREF=$(find "$side" -name 'Preferences' -not -path '*/Extension*' | head -1)
if [ -z "$PREF" ]; then
echo "(no Preferences)" > "diff/preferences-$side.json"
continue
fi
# Normalize key ordering and indentation before diffing.
jq -S . "$PREF" > "diff/preferences-$side.json"
done
diff -u diff/preferences-a.json diff/preferences-b.json > diff/preferences.diff || true
次の差異を確認します:intl.*(ロケール、accept_languages)、profile.default_content_setting_values、webrtc.*、extensions.settings(インストール済み拡張機能のセット)、dns_over_https.*、download.*、safebrowsing.*。
Local Storage(LevelDB)
Local Storage/leveldb/ に対して ls -la を実行しても、ディレクトリが存在するかどうかしか分かりません。LevelDB リーダーを作成せずにキーと値を実際に調べるには、*.log ファイルと *.ldb ファイルに対して strings を実行します。Chrome はキーとほとんどの文字列値を LevelDB 内にプレーンテキストで保存するため、strings | grep を使えば、手間をかけずに必要な情報の 90% を確認できます。
for side in a b; do
LS_DIR=$(find "$side" -type d -name 'Local Storage' -not -path '*/Extension*' | head -1)
if [ -z "$LS_DIR" ]; then
echo "(no Local Storage)" > "diff/localstorage-files-$side.txt"
echo "(no Local Storage)" > "diff/localstorage-origins-$side.txt"
echo "(no Local Storage)" > "diff/localstorage-signals-$side.txt"
continue
fi
# File inventory
ls -la "$LS_DIR/leveldb" > "diff/localstorage-files-$side.txt" 2>&1
# All origins that have any data
strings "$LS_DIR"/leveldb/*.{log,ldb} 2>/dev/null \
| grep -oE 'META:https?://[^[:space:]]+|^_https?://[^[:space:]^]+' \
| sort -u > "diff/localstorage-origins-$side.txt"
# Key inventory per matching origin (adapt grep to the issue host)
strings "$LS_DIR"/leveldb/*.{log,ldb} 2>/dev/null \
| grep -aE '"event"|"User Login"|"track"|sessionGUID|authCookie|expirationDateTime|access_token|id_token|refresh_token' \
| head -100 > "diff/localstorage-signals-$side.txt"
done
diff -u diff/localstorage-files-a.txt diff/localstorage-files-b.txt || true
diff -u diff/localstorage-origins-a.txt diff/localstorage-origins-b.txt || true
diff -u diff/localstorage-signals-a.txt diff/localstorage-signals-b.txt || true
初期化タイミングを追跡する手掛かりとしての分析イベント。 SPA ログインでは、localStorage の *.log ファイルを grep して、埋め込まれた分析キューを確認する価値があります。Mixpanel は JSON イベントデータ(mp_<token>_mixpanel)をインラインで書き込み、Segment は _seg_uid、GA は _ga を書き込みます。JSON 内の User Login、Authenticated、Identify イベントの有無から、プロファイルのスナップショットが取得される前に SPA のログイン後の初期化が実際に完了したかどうかが分かります。キュー内の最後のイベントは、SPA が到達した最後の地点の目安になります。問題のあるプロファイルの最後のイベントが View 2FA で、正常なプロファイルの最後のイベントが User Login なら、問題のあるプロファイルは処理の途中で保存されています。
# Mixpanel example: pull the last few events from each side
for side in a b; do
LS_DIR=$(find "$side" -type d -name 'Local Storage' -not -path '*/Extension*' | head -1)
if [ -z "$LS_DIR" ]; then
echo "(no Local Storage)" > "diff/mp-events-$side.txt"
continue
fi
strings "$LS_DIR"/leveldb/*.{log,ldb} 2>/dev/null \
| grep -oE '"event":"[^"]+"|"Last Login":"[^"]+"|"Timestamp":"[^"]+"' \
| tail -20 > "diff/mp-events-$side.txt"
done
diff -u diff/mp-events-a.txt diff/mp-events-b.txt || true
さらに細かく調べるには、実際の LevelDB リーダー(例:npx -y level-ls、または level パッケージを使った小さな Node スクリプト)を使用します。
セッションストレージ(LevelDB)
URL ハッシュ(app.example.com/#/home など)でルーティングする SPA では、タブ内の認証状態は通常 Cookie ではなく sessionStorage に保存されます。キーには authCookie、expirationDateTime、sessionGUID、access_token、アカウント/テナントの GUID などがあります。認証オリジンに対するこれらの情報が保存済みプロファイルにないと、アプリの起動時に sessionStorage にセッションがないと判断され、Cookie が残っていてもログイン画面に戻されます。ログイン後の SPA の初期化が完了する前にプロファイルをスナップショットした場合に、よく起こる失敗です。
Chrome は、タブごとの名前空間 UUID(namespace-<uuid>-<origin>)に紐付く map-N-<key> エントリとして sessionStorage の名前空間をシリアライズします。正確な N はタブのナビゲーション履歴によって異なりますが、Chrome はプロファイルの読み込み時に名前空間とマップの紐付けを復元するため、プロファイル間で map-N の番号が異なっていても問題ありません。重要なのは、対象のオリジンにキーが存在するかどうかです。
for side in a b; do
SS_DIR=$(find "$side" -type d -name 'Session Storage' -not -path '*/Extension*' | head -1)
if [ -z "$SS_DIR" ]; then
echo "(no Session Storage)" > "diff/sessionstorage-keys-$side.txt"
echo "(no Session Storage)" > "diff/sessionstorage-namespaces-$side.txt"
continue
fi
# Inventory all map-N-* keys (the actual session storage entries)
strings "$SS_DIR"/*.{log,ldb} 2>/dev/null \
| grep -oE 'map-[0-9]+-[A-Za-z._0-9-]+' \
| sort -u > "diff/sessionstorage-keys-$side.txt"
# Origin → namespace UUID mappings (so you know which map-N belongs to which site)
strings "$SS_DIR"/*.{log,ldb} 2>/dev/null \
| grep -oE 'namespace-[a-f0-9-]+-https?://[^/[:space:]]+' \
| sort -u > "diff/sessionstorage-namespaces-$side.txt"
done
diff -u diff/sessionstorage-keys-a.txt diff/sessionstorage-keys-b.txt || true
diff -u diff/sessionstorage-namespaces-a.txt diff/sessionstorage-namespaces-b.txt || true
壊れている側で、認証関連と考えられるキーが欠けていないか確認します。authCookie、expirationDateTime、sessionGUID、userGuid*、userEmail、*_token、access_token、id_token、アカウント/テナント/組織の GUID と考えられるものなどです。正常に動作する側にこれらがまとまって存在し、壊れている側にはログイン前のキー(たとえば __mplss_* の Mixpanel セッションキーのみで、認証キーはないもの)しかない場合、スナップショットの取得が早すぎたということです。
IndexedDB
for side in a b; do
IDB_DIR=$(find "$side" -type d -name 'IndexedDB' -not -path '*/Extension*' | head -1)
if [ -z "$IDB_DIR" ]; then
echo "(no IndexedDB)" > "diff/idb-$side.txt"
echo "(no IndexedDB)" > "diff/idb-sizes-$side.txt"
continue
fi
# Per-origin database directories
ls -la "$IDB_DIR" > "diff/idb-$side.txt" 2>&1
# Sizes per origin (large delta = significant state delta)
du -sk "$IDB_DIR"/* 2>/dev/null | sort -rn > "diff/idb-sizes-$side.txt"
# Cheap content inventory via strings
for db in "$IDB_DIR"/*.indexeddb.leveldb; do
[ -d "$db" ] || continue
origin=$(basename "$db" .indexeddb.leveldb)
strings "$db"/*.{log,ldb} 2>/dev/null \
| grep -aE '"name"|"id"|token|user|session|auth' \
| head -40 > "diff/idb-$origin-$side.txt"
done
done
diff -u diff/idb-a.txt diff/idb-b.txt || true
diff -u diff/idb-sizes-a.txt diff/idb-sizes-b.txt || true
多くのサイトでは、キャッシュやキューに入ったテレメトリに IndexedDB を使いますが、一部の認証フロー(Firebase Auth、Clerk、一部の SaaS アプリ)ではセッショントークンをここに保存します。sessionStorage/localStorage の差分で説明がつかない場合は、確認してみる価値があります。
拡張機能
for side in a b; do
EXT_DIR=$(find "$side" -type d -name 'Extensions' -not -path '*/Local Extension*' | head -1)
if [ -n "$EXT_DIR" ]; then
ls "$EXT_DIR" > "diff/extensions-$side.txt"
else
echo "(no Extensions dir)" > "diff/extensions-$side.txt"
fi
done
diff -u diff/extensions-a.txt diff/extensions-b.txt || true
拡張機能 ID は 32 文字のハッシュです。ID から名前を特定するには、Extensions/<id>/<version>/manifest.json 内の manifest.json を確認します。
ログインデータ(SQLite)
for side in a b; do
LOGIN_DB=$(find "$side" -name 'Login Data' -not -name '*-journal' | head -1)
sqlite3 "$LOGIN_DB" \
"SELECT origin_url, length(username_value) > 0 AS has_user, length(password_value) AS pw_len, date_created
FROM logins ORDER BY origin_url;" \
> "diff/logins-$side.txt" 2>/dev/null || echo "(no Login Data)" > "diff/logins-$side.txt"
done
diff -u diff/logins-a.txt diff/logins-b.txt || true
password_value や username_value は絶対に出力しないでください。出力するのは長さ/真偽値のみです。デバッグ中であっても、保存済みの認証情報は機密情報です。
トップレベルのインベントリ(予期しない差分の検出)
for side in a b; do
(cd "$side" && find . -type f -printf '%P\n' | sort) > "diff/files-$side.txt"
done
diff -u diff/files-a.txt diff/files-b.txt > diff/files.diff || true
これにより、片方のプロファイルには存在するのに、もう片方にはないファイル(Service Worker/ のデータ、IndexedDB/ のデータベース、Trust Tokens など)といった、見落としがちな違いを検出できます。
ステップ 6:調査結果を統合する
観察された各差異を、問題の説明と関連付けます。生の差分をそのままユーザーに見せるのではなく、解釈してください。次のような項目は、根本原因として疑ってください。
- A には認証 Cookie/セッショントークンがあり、B にはない、または期限切れになっている → セッションが確立されていないか、破棄されています。
- A と B の Cookie は一致しているが、
sessionStorageの認証キー(例:authCookie、expirationDateTime、sessionGUID、*Guid*、access_token)が B にない → SPA のログイン後のブートストラップが完了する前にスナップショットが取得されています。これは典型的な保存タイミングの競合です。URL はログイン後のルートに切り替わり、プロファイルは保存されたものの、SPA はまだsessionStorageをハイドレートしていません。次のセッションでは SPA が起動し、Cookie はあるのにタブ内のセッション状態がないことを検出して、ログイン画面に戻します。 - localStorage の分析イベントの末尾が異なる(例:問題のあるプロファイルでは、最後の Mixpanel イベントが 2FA プロンプトなどのフロー途中のステップで、正常なプロファイルでは最後のイベントが
User Login/Authenticated/Identify)→ 認証情報がどのストレージ層にあるかにかかわらず、スナップショットの取得タイミングが問題であることを確認できます。 - ボット検出ベンダーの Cookie(
_abck、datadome、cf_clearance)が片方にしかない → 「正常な」プロファイルではすでにチャレンジがクリアされています。もう一方では再度チャレンジが表示されます。 intl.accept_languagesまたはintl.selected_languagesが異なる → サーバー側のローカライズや地域判定のヒューリスティックによって、処理が分岐します。- 拡張機能が片方にだけインストールされている → 広告ブロッカー、フィンガープリント対策ツール、またはパスワードマネージャーがページの動作を変えている可能性があります。
webrtc.ip_handling_policyが異なる → WebRTC の IP リーク対策の設定が変わり、フィンガープリントが変わる可能性があります。safebrowsingまたはdns_over_httpsが異なる → リクエスト経路に差異が生じています。- A には Login Data の行があり、B にはない → B では自動入力が実行されません。
- Local Storage のオリジンに A ではキーがあり、B にはない → サイト管理のセッション状態(localStorage 内の JWT、機能フラグ、A/B テストのグループ割り当て)が適用されません。
- 認証オリジンの IndexedDB データベース(Firebase、Clerk など)が、B では空で A ではデータがある → SDK のトークンキャッシュがなく、クライアントは再認証するか、処理に失敗します。
ステップ 7:報告する
次のテンプレートを使用してください。
## Profile Diff: <A> vs <B>
### Issue
<one-line restatement of the user's issue description>
### Likely root causes (ranked)
1. **<finding>** — <evidence: which file, which keys/rows> — <why this could cause the issue>
2. **<finding>** — ...
3. **<finding>** — ...
### Other notable differences (probably unrelated)
- <finding>
- <finding>
### Surfaces that matched (for completeness)
- Cookies for <host>: identical
- Preferences `intl.*`: identical
- Extensions installed: identical
- ...
### Suggested next steps
- <e.g. "re-run the failing flow on profile A with a fresh CDP session and capture the request that sets `_abck` — that cookie is the differentiator">
- <e.g. "delete profile B's `Default/Network/Cookies` rows for chase.com and retry — if the flow now works, the stale cookie was the issue">
ステップ 8:クリーンアップ
抽出したプロファイルデータには認証情報が含まれる可能性があります。作業が終わったら、必ずワークスペースを削除してください。
rm -rf "$WORKDIR"
ユーザーがさらに調査するために成果物を残したい場合は、パスを伝え、自分で削除してもらってください。
メモと注意点
- ダウンロード先:
--toには展開先ディレクトリを指定します。.zipファイル名を指定したり、unzip、zstd、tarを実行したりしないでください。CLI が要求されたtar.zst(デフォルト)またはtar形式のレスポンスを自動で展開します。 - JSON はローカルで正規化する:
profiles downloadには--prettyフラグがありません。JSON ファイルを差分比較する前にjq -S .を使い、書式やキーの順序がノイズにならないようにしてください。 - Chrome のプロファイルパスは一定ではありません:データの大半は
Default/の下にありますが、新しい Chrome では Cookie の保存先がDefault/Network/Cookiesに変更されています。パスをハードコードせず、findを使ってください。 - SQLite のロック:プロファイルはスナップショットとしてダウンロードされるため、DB はロックされません。ただし、クエリで "database is locked" エラーが発生した場合は、開いているアーカイブ内のパスではなく、展開したコピーに対してクエリを実行していることを確認してください。
Cache/、Code Cache/、GPUCache/、Service Worker/CacheStorage/は比較しないでください — これらはノイズであり、膨大で意味のない差分が発生します。encrypted_valueとvalue:Chrome は v80 以降、Cookie の本文をencrypted_valueに暗号化します。必ずcookiesテーブルからlength(encrypted_value)を選択してください。length(value)は新しいプロファイルではすべての行で0を返します。- Session Storage の
map-Nの番号はプロファイル間で非決定的です — タブごとの名前空間 ID です。「プロファイル A では認証キーがmap-1-にあり、プロファイル B ではmap-5-にある」といった差異を問題として指摘しないでください。重要なのは、対象のnamespace-<uuid>-<origin>にキーが存在するかどうかです。Chrome はプロファイルの読み込み時に、名前空間とマップの対応関係を復元します。 - エイペックスドメインとサブドメインの Cookie/ストレージ:サイトによっては
.example.com(すべてのサブドメインを対象)に Cookie を設定する一方で、app.example.comのみにsessionStorageを書き込む場合があります。必ずfind $side -name Cookiesと、エイペックスドメインおよび該当するサブドメイン両方のオリジンごとの Local/Session Storage エントリーを確認してください。 - プライバシー:Cookie の値、パスワード欄、または
Login Dataのユーザー名/パスワード列をレポートに出力しないでください。長さ、存在を示すブール値、ホスト名、名前のみを使用してください。