Capturing Behind Authentication
Most interesting screenshots live behind a login page. Dashboards, settings panels, admin interfaces - the stuff worth documenting is usually protected.
Heroshot handles this elegantly. Log in once manually, and it remembers your session for all future captures.
How It Works
When you run heroshot config and log into your app, Heroshot watches. It captures your browser's session state - cookies, localStorage, everything the browser uses to stay logged in.
This session gets encrypted and saved to .heroshot/session.enc. Next time you run heroshot (headlessly, in CI, wherever), it restores that session. The browser starts already logged in.
No login scripts. No credential management. No flaky waits for login forms. Just captured screenshots.
First Time Setup
Run the visual picker and log in manually:
heroshot configA browser opens. Navigate to your login page. Enter your credentials. Complete any 2FA prompts. Do whatever you normally do to log in.
Once you're in, navigate to the pages you want to screenshot. Pick your elements. When you close the browser, Heroshot saves everything - both your screenshot config and your authenticated session.
That's it. Your session is now stored encrypted in .heroshot/session.enc.
Testing It Works
Run a headless capture to verify:
heroshot -vThe verbose flag shows what's happening. You should see it restore your session and capture authenticated pages without any login prompts.
If it redirects to a login page instead, your session wasn't saved correctly or has expired. Run heroshot config again and log in fresh.
Session Expiry
Sessions expire. Your app might invalidate them after a day, a week, or when you log out elsewhere.
The symptom: Screenshots suddenly show the login page instead of the dashboard.
The fix: Re-authenticate:
heroshot config --resetThe --reset flag clears your old session. Log in again in the browser, and you're good.
How often this happens depends on your app:
- Short-lived tokens (hours): You'll re-auth frequently
- Long-lived sessions (weeks): Occasional re-auth
- "Remember me" cookies (months): Rare re-auth
If your app has aggressive session expiry, consider creating a dedicated service account with longer session lifetimes.
CI and Team Usage
Your local machine derives an encryption key automatically. But CI runners and teammates don't share that key. They need it explicitly.
Export Your Session Key
heroshot session-keyThis prints a ~20 character key. Treat it like a password - anyone with this key can decrypt your session.
Add to CI Secrets
For GitHub Actions:
gh secret set HEROSHOT_SESSION_KEY
# Paste your key when promptedOr manually: Repository → Settings → Secrets and variables → Actions → New repository secret.
Use in Workflows
- name: Capture screenshots
run: npx heroshot
env:
HEROSHOT_SESSION_KEY: ${{ secrets.HEROSHOT_SESSION_KEY }}Heroshot automatically uses the key from the environment variable. Your encrypted session file (.heroshot/session.enc) is already in the repo - now CI can decrypt it.
What's in the Session File?
The session file contains:
- Cookies (including auth tokens)
- localStorage data
- sessionStorage data
It's everything Playwright's storageState captures - the full browser state needed to restore a logged-in session.
It does NOT contain:
- Your actual password
- Plain-text credentials
- Your session key (that's stored separately)
The file is encrypted with AES-256-GCM. Without the session key, it's random bytes.
Different Auth Patterns
Standard Username/Password
The simplest case. Log in during heroshot config, session is saved, done.
heroshot config
# Log in normally in the browser
# Pick your screenshots
# Close browserOAuth / SSO
OAuth flows (Google, GitHub, Okta, etc.) work the same way. The redirect dance happens in the browser, Heroshot captures the resulting session.
heroshot config
# Click "Sign in with Google"
# Complete the OAuth flow
# You're now logged in
# Pick screenshots, close browserThe session captures the final authenticated state, regardless of how you got there.
Magic Links
Email-based login where you click a link to sign in.
- Run
heroshot config - Enter your email in the login form
- Check your email (in another window)
- Click the magic link - it opens in the Heroshot browser
- You're logged in
- Pick screenshots, close browser
If the magic link opens in your default browser instead, copy the URL and paste it into the Heroshot browser's address bar.
Two-Factor Authentication
2FA prompts appear during heroshot config. Complete them normally:
- TOTP apps (Google Authenticator, 1Password, etc.): Enter the code when prompted
- SMS codes: Enter the code when it arrives
- Hardware keys (YubiKey): Plug in and tap
The resulting session is saved. Most apps don't require 2FA again until the session expires.
For CI: Make sure your 2FA session is long-lived. Some apps remember devices for 30 days; others require 2FA on every login. If yours requires 2FA every time, you'll need to either:
- Use a service account without 2FA
- Use API tokens instead of browser sessions (see below)
API Tokens / Bearer Auth
Some apps use API tokens instead of cookies. If your app reads auth from localStorage:
- Log in during
heroshot config - The token is saved as part of localStorage
- Future runs restore it automatically
If your app requires a header like Authorization: Bearer <token>, you can inject it:
{
"name": "API Dashboard",
"url": "https://api.example.com/dashboard",
"actions": [
{
"type": "evaluate",
"function": "() => { localStorage.setItem('auth_token', 'your-token-here'); location.reload(); }"
}
]
}For sensitive tokens, consider using environment variables and a pre-capture script instead of hardcoding.
Multiple Accounts
Need screenshots from different user accounts? Different permission levels?
Option 1: Separate config files
# Admin screenshots
heroshot config -c .heroshot/admin.json
# Log in as admin, pick admin-only pages
# User screenshots
heroshot config -c .heroshot/user.json
# Log in as regular user, pick user pages
# Capture both
heroshot -c .heroshot/admin.json
heroshot -c .heroshot/user.jsonEach config has its own session file.
Option 2: Text overrides for user-specific content
If the pages are the same but show different usernames:
{
"name": "Dashboard",
"url": "https://app.example.com/dashboard",
"selector": ".dashboard",
"textOverrides": {
".user-name": "Demo User",
".user-email": "demo@example.com"
}
}This replaces user-specific text with generic placeholders.
Sensitive Data in Screenshots
Screenshots might capture sensitive information - real user data, email addresses, API keys visible on screen.
Use text overrides to sanitize:
{
"name": "Account Settings",
"url": "https://app.example.com/settings",
"selector": ".settings-panel",
"textOverrides": {
".email-display": "user@example.com",
".api-key": "sk-xxxx...xxxx",
".billing-info": "$XX.XX/month"
}
}Or hide sensitive sections:
{
"actions": [{ "type": "hide", "selectors": [".billing-section", ".api-keys"] }]
}Debugging Auth Issues
Session Not Restoring
Symptom: Headless runs show the login page.
Check:
- Does
.heroshot/session.encexist? - Is the session key available? (
heroshot session-keyshould print it) - Is the session expired? Run
heroshot config --resetand re-auth.
CI Can't Decrypt Session
Symptom: CI fails with decryption errors.
Check:
- Is
HEROSHOT_SESSION_KEYset in CI secrets? - Is it passed to the heroshot command via
env:? - Does the key match? Run
heroshot session-keylocally and compare.
Session Works Locally, Fails in CI
Possible causes:
- Different IP address triggers re-auth
- User agent mismatch (CI uses headless Chrome)
- Geographic restrictions
Try:
- Set a custom user agent matching your local browser
- Use the same session for both local and CI
- Check if your app has IP-based session validation
Security Notes
The session file is safe to commit. It's encrypted. Without the session key, it's useless.
The session key is sensitive. Treat it like a password. Don't commit it to your repo. Use CI secrets or environment variables.
Sessions can be powerful. An authenticated session might have access to modify data, not just view it. Consider using a read-only account for screenshots if available.
Rotate periodically. If you suspect a key was exposed, re-run heroshot config --reset to generate a new session and key.
What's Next
- Automated Updates - Full CI setup with authentication
- Troubleshooting Selectors - When elements don't capture correctly
- Actions Reference - Pre-capture actions for complex auth flows