If you've ever exported cookies with curl -c, yt-dlp, a browser extension or an antidetect browser, you've met cookies.txt — the Netscape cookie format. And if you've tried to import that file into something that expects JSON (Cookie-Editor, EditThisCookie, Puppeteer, Playwright, most antidetect browsers), you've probably had to convert it.

Here's how the format works, the one bug almost every quick converter has, and a tiny converter that gets it right.

The Netscape format in 30 seconds

One cookie per line, 7 fields separated by tabs:

.example.com TRUE / TRUE 1893456000 sid abc123 
# Field Example Meaning
1 domain .example.com Cookie domain
2 include subdomains TRUE TRUE = sent to subdomains too
3 path / Cookie path
4 secure TRUE HTTPS only
5 expiration 1893456000 Unix time in seconds, 0 = session cookie
6 name sid
7 value abc123

Lines starting with # are comments. Almost.

The #HttpOnly_ trap

curl, yt-dlp and many browser exporters write HttpOnly cookies like this:

#HttpOnly_.example.com TRUE / TRUE 1893456000 auth_token secret 

It looks like a comment, so the classic one-liner converter skips it:

if (line.startsWith('#')) continue; // 💥 auth_token is gone 

The problem: HttpOnly cookies are usually exactly the ones you need — session and auth tokens. You import the "converted" file, and you're mysteriously logged out.

The fix is to check for the prefix before treating the line as a comment:

let httpOnly = false; if (line.startsWith('#HttpOnly_')) { httpOnly = true; line = line.slice('#HttpOnly_'.length); } else if (line.startsWith('#')) { continue; } 

The JSON side

The JSON format that Cookie-Editor, EditThisCookie and most antidetect browsers import looks like this:

{ "domain": ".example.com", "hostOnly": false, "path": "/", "secure": true, "httpOnly": true, "session": false, "name": "auth_token", "value": "secret", "expirationDate": 1893456000 } 

Two mappings people get wrong:

  • hostOnly is the inverse of "include subdomains". TRUE in the Netscape file means hostOnly: false.
  • Expiration 0 means a session cookie. Set session: true and leave out expirationDate instead of writing expirationDate: 0 (some importers treat that as "expired in 1970" and drop the cookie).

One more edge case: a cookie value can contain a tab. Split the line into fields and join everything after the 6th field back together instead of taking parts[6].

A converter that handles all of it

I put the whole thing into a single-file, zero-dependency converter: stockrush-org/netscape-to-json.

CLI:

npx github:stockrush-org/netscape-to-json cookies.txt > cookies.json # and back npx github:stockrush-org/netscape-to-json --reverse cookies.json > cookies.txt 

Node.js:

const { netscapeToJson, jsonToNetscape } = require('netscape-to-json'); const cookies = netscapeToJson(fs.readFileSync('cookies.txt', 'utf8')); 

Using it with Puppeteer:

const cookies = netscapeToJson(fs.readFileSync('cookies.txt', 'utf8')) .map(({ expirationDate, hostOnly, session, ...c }) => ({ ...c, expires: expirationDate ?? -1, })); await page.setCookie(...cookies); 

If you don't want to install anything, there's a free online version — it runs in the browser, nothing gets uploaded.

TL;DR

  • Netscape cookies.txt = 7 tab-separated fields per line.
  • #HttpOnly_ lines are cookies, not comments — don't skip them.
  • hostOnly = NOT "include subdomains"; expiration 0 = session cookie.
  • Join the value back if it contains tabs.

Found an edge case it doesn't handle? Issues and PRs are welcome.