# Cookies

> Cookies

- Skill: `aibot88/cookies` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add aibot88/cookies`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aibot88/cookies/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: aibot88 (https://skillmd.com/u/aibot88)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/aibot88/cookies

---


# Cookies

`remix/cookie` is the low-level building block. Sessions, auth, and CSRF protection all hang off `Cookie` instances internally — and you can use them directly for anything else (locale prefs, dark-mode flag, A/B bucket, …).

## Imports

```ts
import { createCookie } from 'remix/cookie'
```

## Create a cookie

```ts
const sessionCookie = createCookie('__session', {
  secrets:  ['s3cret1'],         // required for signed cookies
  httpOnly: true,
  secure:   true,
  sameSite: 'Lax',
  path:     '/',
  maxAge:   60 * 60 * 24 * 30,   // 30 days
})
```

| Option     | Notes                                                                    |
|-----------|--------------------------------------------------------------------------|
| `secrets`  | Array of HMAC keys. First entry signs new cookies. Others verify old ones — that's how rotation works (prepend a new key, leave the old one for a transition window). |
| `httpOnly` | Hides the cookie from JS — set to `true` for anything sensitive.          |
| `secure`   | HTTPS-only. `true` in prod, `false` for local HTTP.                        |
| `sameSite` | `'Strict'` / `'Lax'` / `'None'` (capitalised per the type). `'Lax'` is the safe default for top-level nav flows. |
| `maxAge`   | Seconds. Omit for a session cookie (dies with the browser).                |
| `path`     | Defaults to `/`.                                                         |
| `domain`   | Set for cross-subdomain cookies.                                          |

## Parse from a request

```ts
const value = await sessionCookie.parse(request.headers.get('Cookie'))
// value is `null` if the cookie is absent or the signature is invalid
```

## Serialize for a response

```ts
const setCookie = await sessionCookie.serialize({ userId: 42 })

return new Response('OK', {
  headers: { 'Set-Cookie': setCookie },
})
```

Pass an empty value + `maxAge: 0` to clear a cookie.

## Secret rotation

You're rotating a cookie secret. The procedure:

1. **Prepend** the new secret to the `secrets` array. Don't replace.
   ```ts
   secrets: [process.env.NEW_SECRET!, process.env.OLD_SECRET!]
   ```
2. Deploy. All *new* cookies are signed with `NEW_SECRET`. Existing cookies signed with `OLD_SECRET` still verify.
3. After the rollout window (`maxAge` is a good upper bound), drop `OLD_SECRET`.

If you replace instead of prepending, every active session is invalidated.

## Sessions use this under the hood

```ts
import { createCookie } from 'remix/cookie'
import { createCookieSessionStorage } from 'remix/session/cookie-storage'
import { session } from 'remix/session-middleware'

const sessionCookie = createCookie('__session', {
  secrets: [process.env.SESSION_SECRET!],
  httpOnly: true,
  secure:   true,
  sameSite: 'Lax',
})
const sessionStorage = createCookieSessionStorage()

createRouter({ middleware: [session(sessionCookie, sessionStorage)] })
```

The session middleware calls `parse` on the way in and `serialize` on the way out for you.

## A self-contained example — locale preference

```ts
// app/cookies/locale.ts
import { createCookie } from 'remix/cookie'

export const localeCookie = createCookie('locale', {
  maxAge: 60 * 60 * 24 * 365,   // 1 year
  path:   '/',
  sameSite: 'Lax',
  // No `secrets` → unsigned, fine for a non-sensitive preference
})
```

```ts
// reading
const locale = (await localeCookie.parse(request.headers.get('Cookie'))) ?? 'en'

// writing
return new Response(html, {
  headers: { 'Set-Cookie': await localeCookie.serialize('fr') },
})
```

## Common pitfalls

- **Forgetting `secrets` on a session cookie.** Signed cookies *require* `secrets`. Sessions in particular must be signed.
- **Mixing signed and unsigned.** A given cookie is either signed or not — don't flip mid-deployment without rotating the name.
- **`secure: true` over local HTTP.** Browsers silently drop the cookie. Use `secure: process.env.NODE_ENV === 'production'`.
- **Long `maxAge` on JWT-style sessions.** If you store auth state in the cookie itself, treat the cookie as a credential and keep `maxAge` short or pair with a server-side store.

## Further reading

- `references/options.md` — every attribute, with browser-compatibility notes
- `references/rotation.md` — secret-rotation playbook
- See also: [sessions](../sessions/SKILL.md), [auth](../auth/SKILL.md)

