[BUG] Caller-supplied userDataDir is ignored — every session with a custom profile path shares one directory

Author: athul-22Created Aug 24, 2026Updated Aug 24, 2026

Describe the bug

POST /v1/sessions accepts a userDataDir ("User data directory path to use for the session", sessions.schema.ts#L70), and the controller forwards it to SessionService.startSession. The value is then silently discarded: every session that supplies any userDataDir is launched against the single shared persistent profile directory instead.

The cause is operator precedence in session.service.ts#L180-L183:

typescript
const userDataDir =
  options.userDataDir || options.persist === true
    ? path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", "user-data-dir")
    : env.CHROME_USER_DATA_DIR || path.join(os.tmpdir(), "steel-chrome");

|| binds tighter than ?:, so this parses as:

typescript
(options.userDataDir || options.persist === true) ? <persistent dir> : <default dir>

The caller's path is only ever used as a truthiness test. The three-way choice the code reads as — explicit path, else persistent, else ephemeral — is actually a two-way one, and the explicit path is never a possible result.

To reproduce

bash
curl -sX POST localhost:3000/v1/sessions \
  -H 'content-type: application/json' \
  -d '{"userDataDir":"/data/profiles/tenant-a"}'

The Chrome process is launched with --user-data-dir=/app/api/user-data-dir, not /data/profiles/tenant-a. Nothing is written to the requested path, and no warning is logged.

The expression can be evaluated on its own:

javascript
const persistDir = "/app/api/user-data-dir";
const current = (o) =>
  o.userDataDir || o.persist === true ? persistDir : "/tmp/steel-chrome";

current({ userDataDir: "/data/profiles/tenant-a" })              // "/app/api/user-data-dir"
current({ userDataDir: "/data/profiles/tenant-b" })              // "/app/api/user-data-dir"
current({ persist: true })                                       // "/app/api/user-data-dir"
current({})                                                      // "/tmp/steel-chrome"

Expected behavior

  1. userDataDir supplied → that directory is used.
  2. else persist: true → the persistent profile directory.
  3. else → CHROME_USER_DATA_DIR or os.tmpdir()/steel-chrome.

Why this matters

Profiles that are meant to be separate are shared. Two sessions requesting different userDataDir values both land in /app/api/user-data-dir. Whatever the first session leaves behind — cookies, Local Storage, Login Data, tokens in an authenticated profile — is the profile the second session starts from. For anyone using userDataDir to keep per-tenant or per-account profiles apart, the isolation they asked for silently does not exist, and the failure is invisible: the API returns 200 and the session works.

The guard against exactly this is defeated. isSimilarConfig compares userDataDir (validation.ts#L150-L151) so that a change of profile directory forces a relaunch rather than reuse of the running browser (cdp.service.ts#L563-L567). Because both configs collapse to the same string, that comparison can no longer see the difference, and a second session started while a first is still live can be handed the live browser instance of the first.

Sessions persist to disk when the caller did not ask them to. A plain userDataDir request with no persist flag writes a full Chrome profile into the persistent directory inside the install tree (/app/api/user-data-dir in the container image), where it survives the session and accumulates — related to the cleanup work in #324.

CHROME_USER_DATA_DIR is bypassed. The env var added in #194 is on the branch that is never taken when a userDataDir is supplied.

Versions

  • main @ 459be5b..HEAD — introduced in #210 ("feat: add persist, userDataDir option for saving profiles"), still present on current main
  • Affects every released version that exposes userDataDir

Additional context

The precedence itself was previously reported and fixed in #300, which the author closed unmerged before review; the bug is still on main and there is no issue tracking it. I'll open a PR with the parenthesised fix plus regression tests covering all four combinations of userDataDir/persist and asserting the resolved path actually reaches the browser launch config.