# How agent cookie jars keep sessions you did not intend

HTTP clients make cookies feel harmless because browsers have trained us to expect them. An autonomous agent changes the consequence. A response that carries `Set-Cookie` can grant the next request authority that the agent never asked for, never displayed, and may keep after its original credential has changed.

Treat a cookie jar as an authentication store, not as a transport detail. If an API requires cookie sessions, give the jar a named owner, a short lifetime, and an observable boundary. If it does not, turn it off. I have seen too many incident investigations begin with somebody insisting that the request had no credential, while the client quietly sent one in the `Cookie` header.

## A Set-Cookie header can become a second credential

A server sends `Set-Cookie` in a response; a client that owns a jar may send that value in a later `Cookie` header. When the value identifies a server-side session, it is a credential in practical terms. The fact that it arrived after an authenticated request does not make it less capable of authorizing the next one.

This catches teams because they focus on the secret they deliberately supplied: a bearer token, a basic-auth password, or a signed request. They review where that secret comes from and whether it reaches the agent. Then an HTTP library accepts a session cookie without any explicit line of application code. The next request may work after the original Authorization header disappears.

The distinction that matters is between credential injection and session continuation. Credential injection attaches a known secret to one request. Session continuation lets the server issue a new secret and asks the client to replay it later. Both can authorize work. Only one is usually visible in a tool's call arguments.

RFC 6265 describes this exchange as state management: the user agent stores cookie information from `Set-Cookie` and returns applicable cookies in `Cookie`. That wording is deliberately broad because web browsers need it. An agent client should not inherit the browser's habit of keeping state simply because the HTTP protocol permits it.

A cookie also changes the meaning of a failed token rotation. Suppose an agent calls an API with a token, receives `sid=...`, and later loses access to that token. If the API accepts the session cookie on its own, the agent still has a path to the account. The rotation fixed the known credential but did not terminate the issued session. That is a server-side session management question and a client-side containment question. You need to handle both.

## The client decides whether hidden state exists

`Set-Cookie` does nothing by itself. A client must choose to retain it. That choice can hide in a surprising number of places: a reusable HTTP client, a library's cookie manager, a wrapper around fetch, a test harness, or a redirect implementation.

Some common clients do not retain cookies unless you give them a jar. Others retain them when code attaches a standard cookie handler. A shared client can then make a jar shared by accident. Do not infer behavior from the API documentation or from the fact that a call succeeded. Inspect the client construction and run a probe.

A clean test uses a server you control or a harmless test endpoint. The first response should set a clearly named cookie, and the second request should report the headers it received. The result needs to answer two separate questions:

1. Did the client retain the cookie after receiving `Set-Cookie`?
2. Did the client send that cookie on a later eligible request?

With curl, an explicit cookie file makes the state visible:

```
curl -i -c /tmp/agent-cookie-test.txt https://test.example/session/start
HTTP/1.1 200 OK
Set-Cookie: agent_probe=run-7f3; Path=/; Secure; HttpOnly

curl -i -b /tmp/agent-cookie-test.txt https://test.example/session/echo
HTTP/1.1 200 OK

{"received_cookie":"agent_probe=run-7f3"}
```

The file is the point of the exercise. The first command cannot create a hidden session unless something retains its response. The second command cannot replay one unless it receives the file. If your agent tooling gives the equivalent of that file an unnamed process-global home, you have created a session boundary that nobody can reason about during review.

Repeat the probe using the exact runtime path your agent uses, including its request wrapper and redirect settings. A direct curl test proves curl, not your tool. Record whether the jar begins empty, where it lives, and what clears it.

## Reuse across calls is different from reuse across runs

A few calls inside one task may need to share a session. Reusing that session in a later task is a separate decision. Combining those two lifetimes is how a small convenience turns into persistent authority.

Use three boundaries when you assess behavior. First, ask whether a single request needs cookies at all. Second, ask whether related calls in one agent process need them. Third, ask whether a fresh process, a resumed task, another credential, or another human should ever inherit them. Each yes needs a stated reason.

An in-memory jar that disappears with a process is easier to contain than a file in a project directory. A file can survive a crash, a retry, a copied workspace, or a change in who runs the task. It can also end up in a support archive or source-control status output. `HttpOnly` does not protect a cookie file from the client that wrote it; it only limits access from browser script.

I prefer a fresh jar for each agent run even when the same model receives a follow-up prompt. The model's conversational continuity is not a reason to retain HTTP authority. If the follow-up genuinely needs the session, make the operator approve a continued run under the same named boundary, rather than silently letting the last run leave a usable session behind.

Concurrent calls need their own decision too. A shared jar can create order-dependent behavior: request A receives a cookie, request B starts moments later, and B gains a session it never established. That makes reproduction miserable. Give concurrent tasks separate jars unless the API requires a coordinated session and the task explicitly owns it.

## Cookie scope does not match the safety people imagine

Cookie attributes limit delivery, but they do not make a session harmless. Read them as routing instructions from the server to the client, then decide whether your client should honor them at all.

A host-only cookie returns to the exact host that issued it. A cookie with `Domain=example.com` can return to eligible subdomains such as `api.example.com` and `admin.example.com`. RFC 6265 also says a user agent rejects a Domain value that does not domain-match the origin host, but that does not solve the ordinary mistake of trusting every subdomain under a broad corporate domain.

`Path=/billing` limits a cookie to requests whose paths match the cookie path. It does not stop a server at another permitted path from accepting the same cookie if it receives it through some other route, and it does not replace authorization checks. Do not use Path as a security boundary in design discussions. It is a client sending rule.

`Secure` tells the client to send the cookie only over secure transport. `HttpOnly` tells a browser not to expose it through script APIs. Both attributes are good hygiene, but neither limits retention, task sharing, redirects, or the agent's ability to use the cookie. `SameSite` mainly governs browser site context; a non-browser agent should not use it as proof that cross-site behavior is safe.

There is another easy mistake: logging raw headers during debugging. A redactor that removes `Authorization` but leaves `Cookie` has not redacted authentication. Log the presence of a cookie, its name, declared Domain and Path, expiration type, and a nonreversible correlation identifier if you need one. Do not log its value.

## Redirects turn cookie testing into a destination test

A redirect is not merely a different URL. It can change which server sees the next request, whether credentials are retained, and which response gets to set state. Test it as its own flow.

Start with an endpoint that returns a redirect after setting a cookie. Then test each destination separately: the same host, a permitted subdomain, a sibling subdomain, and an unrelated host. Watch both outgoing `Cookie` and outgoing `Authorization`. Different HTTP stacks make different choices, and a wrapper may override the defaults.

A useful fixture produces a short trace such as this:

```
request 1  GET https://api.example.test/start
response 1  302 Location: https://api.example.test/next
            Set-Cookie: probe=A; Path=/; Secure
request 2  GET https://api.example.test/next
            Cookie: probe=A
response 2  200
```

Then change only the Location host. If `api.example.test` redirects to `reports.example.test`, a host-only cookie should not follow. A Domain-scoped cookie may. Your test should state the expected result before it runs, because a trace that surprises you is the point, not an inconvenience to paper over.

Reject cross-origin redirects when the API contract does not require them. If you must follow them, compare the old and new origins, strip request credentials according to an explicit rule, and let a fresh response establish any new cookie state. Never use a broad automatic redirect policy and assume that cookie scope will save you.

## A session test must cross credentials and processes

The test that catches the expensive bug is not `request one, request two`. It crosses the boundaries your operating model claims to enforce.

Build a harmless endpoint that issues a cookie only after it receives a chosen credential label. Have it return the session label on every authorized request. Then run this sequence:

1. Start run A with credential A and receive `sid=A`.
2. Make a second call without credential A but with the same jar.
3. Start run B with credential B and an empty jar.
4. Start run C with no credential and any persisted jar from run A.
5. Revoke credential A or invalidate its session on the test server, then retry run A's jar.

The expected output is a policy decision, not a universal answer. A cookie-based API may intentionally allow the second call within run A. Run B should not see A's state. Run C should fail unless you explicitly approved persistence. After revocation, the test server should reject the old session if your threat model says token rotation must remove active access.

Write the expectation beside the test rather than burying it in a developer's memory. A concise table in a test comment works:

```
run A, same jar, no bearer header: allowed only if session continuation is intended
run B, fresh jar, credential B: must identify as B
run C, persisted jar, no credential: rejected
run A after session invalidation: rejected
```

This exposes a distinction teams often blur: revoking a bootstrap credential and revoking issued sessions are not the same operation. The API owner has to invalidate sessions. The agent tool owner has to avoid preserving them beyond their approved lifetime. Neither side can assume the other handled it.

## Disable implicit jars unless the API needs one

The sensible default for an agent HTTP action is no cookie storage and no automatic `Cookie` header. A response may still contain `Set-Cookie`; record that it happened if your audit design permits, then discard it. The API should use its normal request credential on the next call.

This recommendation is unpopular because many web-adjacent APIs work after a login endpoint only when the client keeps a cookie. People reach for a shared jar because it makes demos and integration tests pass. It is the wrong fix when the documented API supports bearer tokens or another request-scoped mechanism. You are preserving invisible authority to compensate for an integration path you should not have chosen.

If the API truly requires cookies, make the jar an explicit capability. The caller should select a named, empty jar at run start, the tool should report when the response creates or replaces a cookie, and the jar should disappear at the end. Do not let an endpoint opt every agent into a durable session merely by returning a header.

A minimal policy can be plain enough to review:

```
cookie mode: disabled by default
allowed mode: ephemeral per agent run
persistence: prohibited
sharing: prohibited between agent processes
redirects: same-origin only unless the action definition permits another origin
logging: record cookie names and scope, never values
```

That is deliberately less clever than a rules engine. Clever cookie policy grows exceptions until nobody can say which action carries what state. A small set of choices gives reviewers a real answer.

## Audit the state transition, not only the request

An audit trail that lists URLs and status codes will miss the most useful event: a response changed what later calls can do. Capture cookie state changes as first-class events without retaining the session secret itself.

For every HTTP action, the record should make it possible to answer: did the request send cookies, did the response set or clear any, which jar received them, did the jar belong to this run, and did a redirect occur? Cookie names and attributes are usually enough for diagnosis. Store values only if you have a compelling security design for protecting and expiring them, which most agent tools do not need.

When an agent uses Sallyport for HTTP actions, the vault can keep the configured API credential out of the agent while the action executes. That separation is useful only if the HTTP client treats any returned session state with the same suspicion instead of quietly turning it into another credential path.

Audit records also need a human-readable run boundary. If an operator revokes an agent run, they should know whether that act prevents future calls merely through the configured credential or also clears the session state attached to the run. If it does not clear it, say so plainly and make the remaining state inaccessible to a later process.

The first test I would add is intentionally boring: one endpoint sends `Set-Cookie`, the next confirms whether it arrived, and a new agent process repeats the call. Run it before you add retries, redirects, browser compatibility, or a persistent cache. If the answer is not obvious from the trace, your client has more authority than its interface admits.

## API authors can make agents safer without guessing

API authors should document whether cookies are required, what creates them, their intended lifetime, and how clients invalidate them. A sentence that says "use this endpoint after login" is not enough when login may issue a session that outlives the credential used to obtain it.

Offer a request-scoped alternative when possible. Bearer authentication, signed requests, or a narrowly scoped action token often make client behavior easier to audit because each call carries its authority openly. That does not make those mechanisms automatically safe, but it avoids an extra replay channel hidden in client memory.

If you issue a session cookie, make session invalidation work and test it. Token rotation without session invalidation leaves operators with an unpleasant false sense of completion. If you accept both a bearer token and a session cookie, define which one wins when they disagree and expose that choice in diagnostic output.

Do not tell agent builders to imitate browsers unless your service truly depends on browser behavior. Agents make repeated, unattended calls and often operate across different tasks. A browser's long-lived convenience state is a poor default for that environment.

## The safe default is a fresh client with no remembered session

Cookie support is not bad; unowned state is. A short-lived, explicitly selected jar can be the correct way to finish a multi-call API task. A jar that appears because somebody reused an HTTP client is an accident waiting for the right retry, redirect, or credential rotation.

Make the session boundary visible in the tool interface and in the audit record. Then prove it with the cross-run test. The request trace should make a reviewer able to point to every credential path the agent had, including the ones the server tried to hand back.
