Claude CodeでPlaywrightのログイン状態を使い回す — storageStateと秘密情報の扱い
PlaywrightのstorageStateとsetupプロジェクトでログイン済みテストを書き、パスワードとauthファイルをClaude Codeに読ませない権限設計まで、設定例つきで示します。
ログインが必要な画面のE2Eテストを書くとき、Claude Codeに任せる前に決めておくことが2つあります。ログインを1回で済ませて全テストで使い回す構成と、パスワードやセッションをClaudeの目に触れさせない構成です。前者はPlaywrightのstorageStateとsetupプロジェクト、後者はClaude CodeのReadのdenyルールと環境変数で作れます。
この記事では、auth.setup.tsとplaywright.config.tsの書き方、Claudeに書かせるときの指示、.gitignoreとdenyルールの設定、そして両者がぶつかる場面までを順に扱います。hookでテストを自動実行する部分はClaude CodeでPlaywrightのE2Eテストをhookで自動実行するにまとめています。
storageStateとsetupプロジェクトは何をしているか
storageStateは、ブラウザのCookieとlocalStorageをJSONファイルに書き出したものです。Playwrightはテストごとに独立したブラウザコンテキストで動くため、何もしなければ全テストが未ログインで始まります。ログイン後の状態をファイルに保存し、各テストの開始時に読み込ませると、テストごとにログイン操作を繰り返さずに済みます。
保存場所はPlaywright側の推奨が決まっています。playwright/.authディレクトリを作って.gitignoreに入れ、ログイン処理が書き出した状態ファイルをそこへ置きます。
mkdir -p playwright/.auth
echo $'\nplaywright/.auth' >> .gitignoreこの状態ファイルには、本人やテスト用アカウントになりすませるCookieやヘッダーが入りえます。Playwrightの説明も、公開・非公開を問わずリポジトリへのコミットを強く避けるよう求めています。つまりこのファイルは、パスワードと同じ扱いが必要な秘密情報です。後の権限設計は、この一点から導かれます。
全テストで1アカウントを共有する基本形
サーバー側の状態を書き換えないテストなら、全テストで1アカウントを共有する構成が基本形です。ログインだけを担当するsetupプロジェクトを1つ置き、他のプロジェクトがそれに依存する形にします。
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('authenticate', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(process.env.E2E_USER!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
// リダイレクトの途中でCookieが揃うため、最終URLまで待つ
await page.waitForURL('https://example.com/dashboard');
await expect(page.getByRole('button', { name: 'Profile' })).toBeVisible();
await page.context().storageState({ path: authFile });
});Playwrightの例ではユーザー名とパスワードが文字列で直書きされていますが、ここではprocess.envから読んでいます。この差が、Claude Codeと組み合わせるときの最初の分かれ目になります。
設定側では、setupプロジェクトを宣言し、テスト用プロジェクトにdependenciesとstorageStateを指定します。
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});dependencies: ['setup']があるので、setupプロジェクトは常に先に走ります。各テストはpageを受け取った時点でログイン済みです。
落とし穴
UIモードではsetupが走らず、期限切れの状態ファイルが残る
UIモードは、速度のためにsetupプロジェクトを既定では実行しません。認証が切れたら、フィルターでsetupを一時的に有効にしてauth.setup.tsを手動で実行します。状態ファイルが期限切れになったときに自動で消えるわけでもないので、古いuser.jsonを消してから実行し直す場面があります。
認証情報をプロンプトにもCLAUDE.mdにも書かない
Claude Codeにテストを書かせるとき、いちばん楽なのは「ユーザー名はこれ、パスワードはこれ」とプロンプトに貼ることです。避ける理由は単純で、プロンプトに書いた値は会話の履歴に残り、CLAUDE.mdに書けばリポジトリに入るからです。CLAUDE.mdは毎セッションの文脈として読み込まれる前提のファイルなので、そこにパスワードを置くと常にClaudeの目に入る状態になります。
代わりに、Claudeへ渡すのは変数の名前だけにします。値はClaudeを起動するシェルの環境変数か、gitに入れない.env系ファイルに置きます。
# 値はパスワードマネージャー等から取り、シェル履歴に残さない方法で設定する
export E2E_USER="e2e-bot@example.com"
export E2E_PASSWORD="$(pass show e2e/staging)"
claude.envファイルで管理するなら、playwright.config.tsの先頭でdotenvを読み込ませる形が一般的です。
// playwright.config.ts の先頭
import dotenv from 'dotenv';
dotenv.config({ path: '.env.e2e' });.env.e2eは.gitignoreに加え、後述のdenyルールにも入れます。ファイルを読むのはPlaywrightのNodeプロセスだけで、Claudeの組み込みツールは読まない、という分担です。
Claude Codeの設定ファイルにはenvブロックもあり、セッションとそのサブプロセスに環境変数を渡せます。ただし、これはプロジェクトで共有する設定ファイルにも書けるため、パスワードを置く場所には向きません。置くのはE2E_BASE_URLのような公開しても困らない値までにします。
Claudeに渡す指示は「変数名」と「禁止事項」
CLAUDE.mdに書くのは、秘密そのものではなく規約です。次のような断片なら、リポジトリに入れても害がありません。
## E2Eテストの認証
- ログインは tests/auth.setup.ts に1か所だけ書く。各テストでログイン操作をしない
- 認証情報は process.env.E2E_USER と process.env.E2E_PASSWORD から読む。値を直書きしない
- playwright/.auth/ と .env.e2e は読まない・編集しない・コミットしない
- ログインが必要ない画面は test.use({ storageState: { cookies: [], origins: [] } }) で未ログインにする
- 検証は `npx playwright test` を実行し、失敗ログから直す最後の行が実は重要です。Claudeに「ログイン状態のファイルを開いて中身を確かめる」方向へ進ませず、テストを実行して出力を読む反復に固定しているからです。失敗したときに必要なのは状態ファイルの中身ではなく、Playwrightのエラーとトレースです。
authファイルと.envをdenyルールで読ませない
.gitignoreは、コミットを防ぐだけで、Claudeの読み取りは止めません。読み取りを止めるのは、Readのdenyルールです。Read(./.env)のように、パスに対してdenyルールを書くと、Claudeの組み込みファイルツールがそのパスを読めなくなります。
{
"permissions": {
"deny": [
"Read(./playwright/.auth/**)",
"Read(./.env.e2e)",
"Read(./.env)"
]
}
}これを.claude/settings.jsonに置けば、チームで同じ制限を共有できます。パターンはgitignoreの記法で、先頭が./か素のパスなら現在のディレクトリからの相対です。単一の/pathはプロジェクト設定の場合にプロジェクトのルートを指します。ユーザー設定の~/.claude/settings.jsonに同じ/pathを書くと~/.claude基準になるので、全プロジェクトに効かせたいときは//の絶対パスか~/を使います。
denyルールの効き方で押さえておく点は4つです。
Readのdenyは、GrepやGlobなどの読み取りツールと@fileの指定にも、可能な範囲で適用される- 同じパスへのEditとWriteも止まるので、Claudeがauthファイルを新規作成・上書きすることもできなくなる
- Bashでは
cat、head、tail、sedのように対象ファイルが名前で分かるコマンドと、リダイレクト先が対象になる - 拒否に例外を作る
!の否定パターンは、同じ設定ソース内でしか打ち消せない
3つ目の「名前で分かるコマンド」には限りがあります。次の節で、この限界を具体的に見ます。
denyルールが守る範囲と守らない範囲
認証付きE2Eで起こりやすい経路を、denyルールとサンドボックスで比べます。
| Claudeが取りうる経路 | Readのdeny | OSレベルのサンドボックス |
|---|---|---|
Readツールでuser.jsonを開く | Readのdeny止まる | OSレベルのサンドボックス対象外(組み込みツールは権限ルールに従う) |
cat playwright/.auth/user.json | Readのdeny止まる | OSレベルのサンドボックスdenyReadに入れれば止まる |
引数なしのgrep -r token . | Readのdeny止まらない | OSレベルのサンドボックスdenyReadに入れれば止まる |
| Node/Pythonスクリプトが自分でファイルを開く | Readのdeny止まらない | OSレベルのサンドボックスdenyReadに入れれば止まる |
echo $E2E_PASSWORD | Readのdeny止まらない | OSレベルのサンドボックスcredentials.envVarsのdenyで変数を消せる |
権限ルールの説明どおり、denyルールはファイル名を指定しない読み取りや、スクリプトが間接的に開くファイルには及びません。ファイルを読むサブプロセスまで止めたいなら、サンドボックスのfilesystem.denyReadが受け持ちます。サンドボックスの側も、組み込みのReadツールには効かないので、両方を重ねるのが前提の設計です。
{
"permissions": {
"deny": ["Read(./playwright/.auth/**)", "Read(./.env.e2e)"]
},
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["./playwright/.auth", "./.env.e2e"]
}
}
}サンドボックスとテスト実行がぶつかる
ここに落とし穴があります。サンドボックスのdenyは、Claudeが実行するコマンドの子プロセスにも及びます。npx playwright testもその1つなので、playwright/.authをdenyReadに入れると、storageStateを読み込む本体のテストまで読めなくなる設定になります。同様に、credentials.envVarsでE2E_PASSWORDをdenyにすると、サンドボックス内のコマンドからはその変数が消えます。setupプロジェクトがパスワードを読めなくなるわけです。
回避策は2通りあります。
- Claudeに実行させるテストコマンドを
excludedCommandsに載せ、サンドボックス外で動かす。承認フローは通常の権限ルールに戻る - サンドボックスのdenyは入れず、
Readのdenyだけで運用する。守る範囲は狭くなるが、テスト実行は素直に動く
どちらが合うかは、テスト用アカウントの権限の重さで決まります。本番相当の権限を持つアカウントを使うなら1、使い捨てのステージング用アカウントなら2で足りる、というのが筋の通った分け方です。アカウント自体を最小権限にしておけば、万一漏れても被害が小さくなります。
認証付きE2Eの権限設計を決める順番
- 1
テスト用アカウントを分ける
本番の個人アカウントは使わず、ステージング専用の最小権限アカウントを用意します。
- 2
値は環境変数に置く
auth.setup.tsはprocess.envだけを読み、CLAUDE.mdには変数名と規約だけを書きます。 - 3
Readのdenyで状態ファイルと.envを塞ぐ
playwright/.auth/**と.env.e2eをpermissions.denyに入れ、.gitignoreにも同じ行を置きます。 - 4
サンドボックスを併用するか判断する
併用するなら、テスト実行コマンドがdenyに引っかからないか、実際に
npx playwright testを通して確かめます。
ロールが複数あるとき、ログイン不要の画面があるとき
1アカウントの共有は、並列実行で互いのデータを壊さないテストにだけ向きます。設定画面の描画を確かめるテストと、設定を書き換えるテストが同時に走るような構成では、アカウントを分ける必要があります。
| 状況 | 向く構成 | 秘密情報の増え方 |
|---|---|---|
| サーバー側の状態を変えない | 向く構成setupプロジェクトで1アカウント共有 | 秘密情報の増え方状態ファイル1つ、資格情報1組 |
| テストが共有状態を書き換える | 向く構成workerごとに1アカウント(worker scopeのfixtureでstorageStateを上書き) | 秘密情報の増え方ワーカー数ぶんの資格情報と状態ファイル |
| 管理者と一般ユーザーなど複数ロール | 向く構成ロールごとに状態ファイルを分けて保存し、テストごとに指定 | 秘密情報の増え方ロール数ぶんの資格情報と状態ファイル |
| ログイン画面そのものをテストする | 向く構成そのファイルだけstorageStateを空にする | 秘密情報の増え方増えない |
worker単位のアカウント分けでは、Playwrightの例はtestInfo.parallelIndexでワーカーを区別し、状態ファイルをoutputDir配下の.auth/<番号>.jsonに書き出します。ここでもReadのdenyは必須で、パターンをplaywright/.auth/**だけにすると、test-results配下に出る状態ファイルを塞ぎ損ねます。出力先を変えたなら、denyのパターンも合わせて直します。
ロールごとに状態ファイルが増えると、denyの漏れも増えます。保存先をplaywright/.auth/admin.json、playwright/.auth/member.jsonのように1つのディレクトリに集めておくと、Read(./playwright/.auth/**)の1行で全部をカバーできます。
ログイン不要な画面は、ファイル単位で未ログインに戻せます。
// tests/not-signed-in.spec.ts
import { test } from '@playwright/test';
test.use({ storageState: { cookies: [], origins: [] } });
test('未ログインでログインページへ飛ばされる', async ({ page }) => {
await page.goto('https://example.com/dashboard');
// ...リダイレクト先を検証する
});Claudeに実際に頼むときの流れ
ここまでを前提にすると、依頼は短くなります。
tests/auth.setup.ts を作って、ログイン状態を playwright/.auth/user.json に保存してください。
認証情報は E2E_USER と E2E_PASSWORD の環境変数から読みます。
playwright.config.ts には setup プロジェクトと依存関係を足してください。
終わったら npx playwright test を実行し、失敗したらログから直してください。値はプロンプトに含まれていないので、会話履歴に残る心配がありません。生成物が公式の例と同じ形になるとは限らないため、process.envから読んでいるか、storageStateのパスが.gitignoreの対象になっているかを、差分で目視確認します。
ログイン画面のセレクターが分からないときは、Claudeに推測させるより、ブラウザを操作できるPlaywright MCPで画面を見せるほうが確実です。使い方はPlaywright MCPサーバーの使い方で扱っています。モバイルアプリ側の同種のテストはClaude CodeでMaestroのモバイルE2Eテストを書くが近い題材です。
よくあるつまずき
テストが毎回未ログインで始まる
dependencies: ['setup']とuse.storageStateの両方が、そのプロジェクトに書かれているかを見ます。片方だけだと、setupは走っても状態を読み込まない、または読み込むファイルがまだ存在しない状態になります。
状態ファイルを作ったのに、ログイン後のページへ行けない
ログイン処理の途中で複数回リダイレクトされる場合、Cookieが揃う前に保存してしまうことがあります。waitForURLで最終URLまで待ってから、storageStateを書き出します。
denyルールを書いたのにClaudeが中身を触れてしまう
ファイル名を指定しない読み取りや、スクリプトが自分でファイルを開く経路には、denyルールが及びません。守りたいなら、サンドボックスのdenyReadを併用するか、そもそも状態ファイルをリポジトリの外のディレクトリに置きます。パターンの基準が違って効いていない場合もあります。ユーザー設定にRead(/playwright/.auth/**)と書くと、プロジェクトではなく~/.claude基準で解釈されます。
状態ファイルが古くなってテストが落ちる
Cookieの期限が切れると、setupを通さない実行では未ログイン扱いになります。古いuser.jsonを削除してsetupを実行し直すのが基本です。毎回の実行で状態を保持する必要がないなら、testProject.outputDir配下に書き出す方法もあります。実行前に自動で掃除されます。
まとめ
storageStateとsetupプロジェクトは、ログインを1回にまとめる仕組みです。Claude Codeと組むときは、そこに「資格情報を渡さない」「状態ファイルを読ませない」の2点が加わります。auth.setup.tsを環境変数だけで書き、CLAUDE.mdには規約だけを置き、Readのdenyでplaywright/.authと.env.e2eを塞ぐところまでが最小構成です。
その先は、テスト用アカウントの権限の重さで決まります。最小権限の使い捨てアカウントなら、denyルールまでで足ります。強い権限のアカウントを使うなら、サンドボックスを重ねたうえで、テスト実行コマンドだけをexcludedCommandsで外に出す構成が候補です。設定ファイル全体の考え方はClaude Code settings.json完全ガイドが参考になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。