
How to Redact HAR Files Safely (Keep Tests Shareable, Remove Secrets)
HAR captures are great raw material for regression tests because they reflect what actually hit your backend. They are also one of the easiest ways to accidentally leak credentials, session cookies, API keys, CSRF tokens, and user data into Slack, issues, or Git.
This guide covers a practical, developer-friendly way to redact HAR files safely so you can keep tests shareable (reviewable in pull requests, runnable in CI) without shipping secrets.
Why HAR files are uniquely dangerous
A HAR file is effectively a bundle of:
- Full request URLs (including query strings)
- Request headers (often including
AuthorizationandCookie) - Request bodies (login payloads, tokens, PII)
- Response headers and bodies (tokens, user profiles, emails, IDs)
Unlike many API-client exports, HAR captures are “everything that happened”, not “what you intended to keep”. The HAR 1.2 spec makes it clear that entries can include full content payloads.
If your goal is “generate deterministic API tests that can live in Git”, your goal is not “commit the HAR”. Your goal is “extract the behavior, parameterize the secrets, drop the rest”.
A safe workflow in one sentence
Keep the raw HAR local, sanitize it automatically, generate YAML tests, commit only the YAML (plus non-secret fixtures), and inject secrets at runtime via CI or env.

Export the HAR from Chrome (and what Chrome already redacts)
Redaction starts in the Network panel, because what you capture decides how much there is to clean up later. If you want the capture-and-replay side of this in more depth, HAR File API Testing covers it.
- Open the page you want to record. Open DevTools by right-clicking the page and choosing Inspect, or with
F12on Windows and Linux orCmd + Option + Ion a Mac. - Switch to the Network tab.
- Check Preserve log. Chrome's reference puts it plainly: "To save requests across page loads, check the Preserve log checkbox on the Network panel." Without it, a redirect after login wipes the request you came for.
- Click Clear to empty the request list, so the file starts at the beginning of your workflow instead of halfway through a page load.
- Run only the workflow you want to test: sign in, create the resource, read it back, delete it. Every other tab and widget you touch lands in the same file.
- Filter the list down to your API host before you export, so static assets and third-party beacons never reach disk.
- Export. Chrome documents two routes: right-click any request and pick Copy > Save all [listed] as HAR (sanitized) or Save all [listed] as HAR (with sensitive data), or use the Export HAR (sanitized)... button in the action bar at the top of the panel.
That last step is where the confusion lives, because those two exports are not the same file.
Chrome's documentation describes the default: "By default you can export the 'sanitized' network log in HAR format that excludes sensitive information such as Cookie, Set-Cookie, and Authorization headers." The other export is opt-in. You have to turn on Settings > Preferences > Network > Allow to generate HAR with sensitive data first, and only then select Export HAR (with sensitive data) from the drop-down menu.
So you are starting from one of two files, and they need different amounts of work:
- The sanitized export. Chrome has already taken out the three headers it names. Read the wording closely: "such as" is a list you can count on, not a promise about everything a HAR can carry. Query strings, request bodies and response bodies are still yours to check.
- The export with sensitive data. You opted in, usually because the flow only replays with real auth. Treat that file like a credential from the moment it lands on your disk, and run it through a sanitizer before it goes anywhere.
Neither file is shareable as it comes out. A sanitized export is a smaller cleanup job, not a finished one.
What to redact (and what to keep)
Redaction is easiest if you start from an explicit policy: an allowlist for what is safe, and a denylist for what is never safe.
High-risk fields you should treat as secrets by default
| Location in HAR | Typical keys | Why it’s risky | Safer replacement |
|---|---|---|---|
| Request headers | Authorization, Cookie, X-API-Key, X-Auth-Token | Direct credential material | Replace with {{ENV_VAR}} or obtain via login step |
| Response headers | Set-Cookie | Session fixation, replay | Drop entirely in committed artifacts |
| Query string | token, key, code, signature, X-Amz-Signature | Credentials often passed in URL | Replace with {{ENV_VAR}} or re-generate in test |
| Request body | password, client_secret, refresh_token | Credential material, PII | Replace with {{ENV_VAR}} or fixture with dummy values |
| Response body | access_token, id_token, user objects | Tokens and PII | Drop body, or store sanitized fixture (if needed) |
Data you usually should keep
Keep the minimum needed to reproduce and assert behavior:
- Method, path, and stable query params
- Stable headers (often
Content-Type,Accept) - JSON bodies, but with secrets replaced
- A small set of assertions that don’t depend on volatile values
Data you usually should drop for determinism
Dropping these makes CI runs reproducible:
Cookie,Set-CookieContent-Length(will change after redaction)User-Agent,sec-ch-*,Origin,Referer(often irrelevant for API regression)- Tracing headers (
x-request-id,traceparent) and timestamps
Redaction strategy: minimize, parameterize, then chain
There are three patterns that keep tests shareable:
1) Minimize captured scope
Even if you already have a HAR, you can aggressively reduce what you convert into tests:
- Filter to a single domain or set of API hosts.
- Drop static assets and third-party calls.
- Keep only the requests that represent a workflow boundary (login, create resource, fetch resource, cleanup).
(The Chrome walkthrough above covers the capture itself. DevTools also keeps a longer guide on generating a HAR file in Chrome safely, and the HAR import docs cover what happens to the file once it is clean: the requests get extracted, organized by domain and path, and turned into a flow with variable mappings generated for you.)
2) Parameterize secrets into environment variables
When something must be provided externally (API keys, client secrets, long-lived tokens), replace the literal value with an env var placeholder.
Example pattern in YAML:
env:
API_BASE_URL: '{{API_BASE_URL}}'
API_TOKEN: '{{API_TOKEN}}'
steps:
- request:
name: ListProjects
method: GET
url: '{{API_BASE_URL}}/v1/projects'
headers:
Accept: application/json
Authorization: 'Bearer {{API_TOKEN}}'
- if:
name: CheckListProjects
condition: 'ListProjects.response.status == 200'
then: ListProjects
depends_on: ListProjects
This keeps the flow reviewable in Git, and your CI can inject {{API_TOKEN}} from its secret store.
3) Replace replayed sessions with request chaining
The most common HAR leak is “it worked because the browser had a session cookie”. Don’t commit that. Instead, make auth explicit:
- A login step that returns a token
- Reference the token directly from the login step's response body
- Use it on subsequent requests via
{{Login.response.body.access_token}}
Example using the DevTools YAML format:
env:
API_BASE_URL: '{{API_BASE_URL}}'
TEST_USERNAME: '{{TEST_USERNAME}}'
TEST_PASSWORD: '{{TEST_PASSWORD}}'
RUN_ID: '{{RUN_ID}}'
steps:
- request:
name: Login
method: POST
url: '{{API_BASE_URL}}/auth/login'
headers:
Content-Type: application/json
body:
username: '{{TEST_USERNAME}}'
password: '{{TEST_PASSWORD}}'
- js:
name: ValidateLogin
code: |
export default function(ctx) {
if (ctx.Login?.response?.status !== 200) throw new Error("Login failed");
return { validated: true };
}
depends_on: Login
- request:
name: CreateWidget
method: POST
url: '{{API_BASE_URL}}/v1/widgets'
headers:
Authorization: 'Bearer {{Login.response.body.access_token}}'
Content-Type: application/json
body:
name: 'ci-{{RUN_ID}}'
depends_on: Login
- js:
name: ValidateCreate
code: |
export default function(ctx) {
if (ctx.CreateWidget?.response?.status !== 201) throw new Error("Expected 201");
return { validated: true };
}
depends_on: CreateWidget
- request:
name: GetWidget
method: GET
url: '{{API_BASE_URL}}/v1/widgets/{{CreateWidget.response.body.id}}'
headers:
Authorization: 'Bearer {{Login.response.body.access_token}}'
depends_on: CreateWidget
- if:
name: CheckGetWidget
condition: 'GetWidget.response.status == 200'
then: GetWidget
depends_on: GetWidget
The key point: the test is shareable because it's no longer "replay this exact browser session". It's "execute an API workflow".
A practical HAR redaction policy (that won’t break conversions)
Instead of chasing every possible secret pattern, start with a deterministic policy.
Header policy
- Delete headers by name (case-insensitive) if they are known secret carriers.
- Prefer allowlisting stable headers when possible.
Denylist candidates:
authorizationcookieset-cookiex-api-keyx-auth-tokenproxy-authorization
Also delete volatility headers to reduce diff noise:
content-lengthdateexpiresif-none-matchif-modified-sincex-request-idtraceparent
Query string policy
- If a parameter name matches common secret names (
token,key,signature,password,code), redact. - If it is an OAuth authorization code or signed URL, you typically should not convert it directly into a committed test. Re-generate it via the auth flow.
Body policy
- For JSON, traverse keys and redact by key name match.
- For form-encoded bodies, redact values by key.
- For GraphQL, redact variables payload fields (many teams accidentally commit user emails and auth headers here).
Response content policy
If your HAR was exported “with content”, responses can leak PII and tokens.
- Default: drop response bodies from anything you plan to share.
- Exception: keep small, sanitized fixtures only when assertions depend on specific response shapes.
Automate redaction (do not do it by hand)
Manual editing is how secrets slip through review. Automate redaction so it is:
- Repeatable
- Reviewable (diffable)
- Enforceable (hooks and CI checks)
Minimal sanitizer approach
A sanitizer can be simple:
- Parse HAR JSON
- For each entry:
- Redact header values based on header name
- Redact query params by key name
- Redact request body fields by key name (when JSON)
- Optionally delete response content
Here is that list as something you can actually run. The HAR spec fixes the shape you are walking: the root object "MUST be present and its name MUST be log", and its entries field is "an array of objects of type entry, each representing one exported (tracked) HTTP request". Inside an entry, request.headers is a "List of header objects" and request.queryString a "List of query parameter objects", so a redactor walks those lists instead of the raw URL text.
One jq pass covers the headers, the query string, the cookies and the bodies:
jq '
["authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token", "proxy-authorization"] as $secret_headers
| ["token", "key", "code", "signature", "password", "access_token", "client_secret"] as $secret_params
| def scrub($names):
[ .[]?
| (.name | ascii_downcase) as $n
| if ($names | index($n)) then .value = "REDACTED" else . end ];
.log.entries |= [ .[]
| .request.headers |= scrub($secret_headers)
| .response.headers |= scrub($secret_headers)
| .request.queryString |= scrub($secret_params)
| .request.cookies = []
| .response.cookies = []
| if .request.postData.params then .request.postData.params |= scrub($secret_params) else . end
| if .request.postData.text then .request.postData.text = "REDACTED" else . end
| if .response.content.text then .response.content.text = "REDACTED" else . end
]
' capture.har > capture.clean.har
Two details in there are worth knowing. A posted body shows up in the HAR twice: postData.params is the "List of posted parameters (in case of URL encoded parameters)" and postData.text is the "Plain text posted data", so scrubbing one and not the other leaves the secret sitting in the file. And response.content.text holds the "Response body sent from the server or loaded from the browser cache", which is why dropping it by default is the safe move.
That pass rewrites secret values but keeps every header it has no opinion about, Content-Length included. When you want the volatility headers gone and JSON bodies walked key by key, a short script goes further:
// sanitize-har.mjs - usage: node sanitize-har.mjs capture.har > capture.clean.har
import { readFileSync } from "node:fs";
const REDACTED = "REDACTED";
const SECRET_HEADERS = new Set([
"authorization", "cookie", "set-cookie",
"x-api-key", "x-auth-token", "proxy-authorization",
]);
const DROP_HEADERS = new Set([
"content-length", "date", "expires",
"if-none-match", "if-modified-since", "x-request-id", "traceparent",
]);
const SECRET_KEY =
/^(token|key|code|signature|password|access_token|refresh_token|id_token|client_secret)$/i;
const scrubPairs = (pairs) =>
(pairs ?? [])
.filter((pair) => !DROP_HEADERS.has(pair.name.toLowerCase()))
.map((pair) =>
SECRET_HEADERS.has(pair.name.toLowerCase()) || SECRET_KEY.test(pair.name)
? { ...pair, value: REDACTED }
: pair,
);
const scrubJson = (node) => {
if (Array.isArray(node)) return node.map(scrubJson);
if (node && typeof node === "object") {
return Object.fromEntries(
Object.entries(node).map(([k, v]) => [k, SECRET_KEY.test(k) ? REDACTED : scrubJson(v)]),
);
}
return node;
};
const har = JSON.parse(readFileSync(process.argv[2], "utf8"));
for (const entry of har.log.entries) {
entry.request.headers = scrubPairs(entry.request.headers);
entry.response.headers = scrubPairs(entry.response.headers);
entry.request.queryString = scrubPairs(entry.request.queryString);
entry.request.cookies = [];
entry.response.cookies = [];
const post = entry.request.postData;
if (post?.params) post.params = scrubPairs(post.params);
if (post?.text) {
try {
post.text = JSON.stringify(scrubJson(JSON.parse(post.text)));
} catch {
post.text = REDACTED;
}
}
if (entry.response.content?.text) entry.response.content.text = REDACTED;
}
process.stdout.write(JSON.stringify(har, null, 2));
Run it on the capture you are about to share, then open the output and read it. Matching on key names catches password and access_token; it will not catch an email sitting in a username field, so the eyeball pass stays part of the job.
If you already use secret scanners, run them on the sanitized output as a backstop.
Good options:
- Gitleaks (fast, easy to run in CI)
- TruffleHog (strong detectors, good for repo scanning)
Two HAR-specific tools come up often enough to be worth naming:
- Google's HAR Analyzer takes an upload: "After uploading your HAR file, you can click the redaction button in the top left corner to download a version of the HAR file with sensitive data redacted." Handy for a one-off file you are sending to a vendor.
- google/har-sanitizer "collects the names and values of all passwords, cookies, headers, URLQuery/POSTData/HTML-Form parameters, and embedded content mimetypes, and redacts values either already known to be sensitive, or those specified by the user". Check the state of it before you adopt it: the repository was archived on 18 April 2026 and is read-only, so nobody is shipping fixes to it.
Keep Git clean: what to commit vs what to ignore
A stable team convention:
- Do not commit raw HAR files.
- Commit generated YAML flows.
- Commit non-secret fixtures (sanitized JSON responses, if needed).
A basic .gitignore stance (adapt to your repo):
# Raw captures
*.har
captures/
# Local env files
.env
*.local.yml
Add a pre-commit guardrail (YAML example)
If your team uses pre-commit, you can block accidental HAR commits and run scanners locally.
repos:
- repo: local
hooks:
- id: block-har
name: Block committing HAR files
entry: bash -c 'git diff --cached --name-only | grep -E "\\.har$" && echo "Do not commit HAR files" && exit 1 || exit 0'
language: system
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.0
hooks:
- id: gitleaks
This doesn’t replace careful engineering, but it makes failures loud and early.
How this differs from Postman, Newman, and Bruno
HAR-based workflows and “API client collections” solve different problems, but teams often mix them.
Postman and Newman
- Postman collections and environments are exportable, but secrets often end up in environment JSON, local values, or synced workspaces.
- Newman runs collections in CI, but the data model is still “collection + environment + scripts”. It works, but review diffs can be noisy, and secret handling becomes a process issue.
Bruno
- Bruno is closer to “tests as files”, and it avoids some cloud coupling.
- However, you still need conventions to keep secrets out of request files and to make request chaining deterministic.
YAML-first flows (and why it matters for redaction)
With YAML-first API testing, you can make redaction and shareability structural:
- The committed artifact is human-readable YAML.
- Secrets become obvious placeholders (
{{API_TOKEN}}), not opaque values buried in exported JSON. - Request chaining is explicit via node output references (
{{NodeName.response.body.field}}), so you don't need to preserve browser cookies from the HAR.
If you’re converting captured traffic into Git-native tests, the important transition is: from “captured session replay” to “declarative workflow with explicit inputs”.
If you are weighing up the tools that do that conversion for you, Stresseur, the hosted AI test engineer from the dev.tools team, keeps a comparison of four tools that build tests from real traffic.
For a concrete “HAR to YAML to CI” path, see HAR to YAML regression tests.
Common redaction pitfalls (and how to avoid them)
Redacting a value but leaving a valid session cookie shape
If you keep Cookie: session=REDACTED, some servers treat it as an invalid session and return a different code path than an unauthenticated request.
Preferred fixes:
- Remove the
Cookieheader entirely. - Replace cookie-based auth with a login request step and reference the token from its response body.
Keeping Content-Length
After you redact bodies, Content-Length is wrong. Many servers ignore it for typical clients, but some stacks and proxies do not.
Fix: drop Content-Length from captured headers.
Redacting CSRF tokens without replacing the flow
If your web app requires CSRF tokens, they are typically generated per session.
Fix: model the CSRF acquisition step (fetch page or preflight endpoint), then reference the token from its response body in subsequent steps. Do not hardcode it from the HAR.
Leaving secrets in response content
Teams often redact only request headers and miss:
access_tokenin JSON responses- password reset links
- email addresses and IDs
Fix: drop response bodies by default and add back only sanitized fixtures when needed.
A “shareable tests” checklist for teams
Before you upload or PR anything derived from har files:
- No raw
.harin Git. - No
Authorization,Cookie,Set-Cookiein committed artifacts. - No tokens, emails, or reset links in response bodies.
- Secrets are only referenced via env placeholders.
- Auth is explicit (request chaining), not “replay browser state”.
- CI injects secrets, and logs do not print sensitive headers or bodies.
If you want a Git workflow framing for this, the PR checklist approach in GitHub Flow Explained for API Testing Teams pairs well with “tests must be reviewable and safe to share”.
Frequently Asked Questions
Should I ever commit a HAR file to a repo? Generally no. Treat raw HAR files like packet captures: useful locally, unsafe as a shared artifact. Commit the sanitized YAML flow instead.
Is replacing secrets with REDACTED enough? Not reliably. You also need to remove session-dependent headers (cookies) and rework auth into explicit request chaining, otherwise tests won’t be deterministic.
What’s the safest way to share a HAR with a vendor or another team? Share only a sanitized HAR with response bodies removed, and run a secret scanner on it first. Better: share a YAML flow that references env vars instead of containing credentials.
How do I handle OAuth flows captured in a HAR? Don’t commit authorization codes or signed redirect URLs. Model the token exchange explicitly (or inject a client credential via env), then capture the access token during the test run.
Why does my test fail after redaction even though the HAR worked? The HAR likely depended on browser state (cookies, CSRF tokens, cached auth). Fix it by deleting those headers and making the state transitions explicit via chaining and captures.
Does Chrome’s sanitized export mean the file is safe to share? No. Chrome’s documentation says the sanitized export “excludes sensitive information such as Cookie, Set-Cookie, and Authorization headers”. That covers three headers; it says nothing about tokens in query strings, credentials in request bodies, or user data in response bodies, which is the rest of this article.
How do I open a HAR file in Google Chrome? Chrome loads one straight back into the Network panel: drag the file into the Requests table, or click Import HAR in the action bar at the top of the panel. Reopening your sanitized file this way is the quickest check on what the redaction pass actually left behind before you send it.
Turn a HAR into a reviewable YAML test (without leaking secrets)
If your end goal is Git-native API regression tests, the clean path is: capture traffic, convert it into a YAML workflow, replace secrets with environment variables, and make auth and request chaining explicit.
DevTools is built around that workflow: it converts HAR captures into human-readable YAML flows that are easy to review in pull requests and run locally or in CI. Start from the safe capture guide (Generate a HAR file in Chrome) and then follow the end-to-end example (HAR to YAML for CI).
