> ## Documentation Index
> Fetch the complete documentation index at: https://hub.hcompany.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Bring your own browser profile

> Start a browser session from saved cookies and storage.

A **browser profile** captures a browser's saved state (cookies, local storage, and session data) as a reusable, organization-scoped bundle. Upload one once, then set [`browser_profile_id`](/computer-use-agents/browser/configuration) so the agent starts a session already authenticated instead of signing in from scratch every run. Sessions can also [write their final state back](#persisting-state-back-into-the-profile) into the profile, so it stays fresh from run to run instead of aging out.

Don't want to manage profiles at all? Your [default profile](#default-profiles) gives you a browser that remembers you across sessions with zero setup — no upload, no ids to track.

Where a [vault](/computer-use-agents/vaults/overview) injects individual secrets at the moment the agent fills a login form, a profile restores the **entire** logged-in state up front. Reach for a vault when the agent should sign in with credentials; reach for a profile to carry an existing, already-authenticated session forward.

Profiles belong to your organization: any agent in the org can load one, and they persist across sessions until you delete them.

## Creating a profile

A profile is built from a browser's **user-data directory**: the folder Chrome uses to store cookies, local storage, saved logins, and extensions.

<Warning>
  Don't upload your personal Chrome directory (`~/Library/Application Support/Google/Chrome` on macOS, `~/.config/google-chrome` on Linux). It holds every password, session token, and history entry from your day-to-day browsing.
</Warning>

The simplest way to get a clean directory is to launch a **separate Chrome instance** pointed at a throwaway `--user-data-dir`. This runs alongside your normal browser without touching your real profile.

<Steps>
  <Step title="Launch a fresh Chrome instance">
    Start Chrome with a new empty directory. It opens a brand-new profile with no cookies, logins, or extensions.

    `--password-store=basic` is what makes the profile portable: it tells Chrome to encrypt cookies and saved passwords with a built-in key instead of your OS keychain. The session runner uses the same store, so cookies saved this way decrypt correctly when the agent loads the profile. Without it, cookies are encrypted with a machine-specific key that the runner can't reproduce, and the agent starts logged out.

    <CodeGroup>
      ```bash macOS theme={null}
      "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
        --user-data-dir="$HOME/agent-profile"
      ```

      ```bash Linux theme={null}
      google-chrome \
        --user-data-dir="$HOME/agent-profile" \
        --password-store=basic
      ```
    </CodeGroup>

    <Warning>
      `--password-store=basic` only takes effect on **Linux**. On **macOS and Windows** Chrome always encrypts cookies with the OS keychain (Keychain / DPAPI), so a profile built with desktop Chrome there loads **without its auth cookies** — no flag can change that. To carry cookies forward on macOS or Windows, build the profile in Docker instead (see [macOS and Windows: build the profile in Docker](#macos-and-windows-build-the-profile-in-docker-to-keep-cookies)).
    </Warning>
  </Step>

  <Step title="Set up the state the agent needs">
    In that window, do exactly what you want the agent to inherit: log in to the target sites, dismiss cookie banners, install extensions, adjust settings. Everything is written into `~/agent-profile`.
  </Step>

  <Step title="Close Chrome and zip the directory">
    Cleanly quit the browser so it flushes its state to disk, then archive the directory:

    ```bash theme={null}
    cd "$HOME/agent-profile"
    zip -r ../profile.zip .
    ```

    The resulting `profile.zip` is what you upload below.
  </Step>
</Steps>

### macOS and Windows: build the profile in Docker to keep cookies

`--password-store=basic` has no effect on macOS or Windows, so a profile built with desktop Chrome there loses its cookies. Build it inside a Linux container instead — the container has no OS keychain, so Chromium encrypts cookies with the same portable key the session runner uses. This needs [Docker](https://www.docker.com/).

```bash theme={null}
# Start a throwaway Linux Chromium you can drive from your browser.
# The tag is pinned to Chromium 150.0.7871.124 — the version the session runner
# uses. Don't use :latest: it tracks a newer Chromium, and a profile built on a
# newer version won't load on the runner.
docker run -d --name agent-profile -p 127.0.0.1:3000:3000 \
  -e CHROME_CLI="--password-store=basic" \
  -v "$HOME/agent-profile:/config" \
  lscr.io/linuxserver/chromium:11e602ae-ls45

# Open http://localhost:3000 and log in to the sites the agent needs.

# Signal Chromium to flush cookies to disk, wait for it, then stop and zip:
docker exec agent-profile pkill -INT -o chromium
sleep 3
docker rm -f agent-profile
cd "$HOME/agent-profile/.config/chromium" && zip -r ~/profile.zip .
```

<Warning>
  Send Chromium `SIGINT` (`pkill -INT`) and give it a moment **before** stopping the container. Chromium only writes cookies to disk on a clean shutdown or an occasional autosave — stopping the container straight away (`docker stop` sends `SIGTERM`, `docker rm -f` sends `SIGKILL`) can discard the logins you just created.
</Warning>

Upload the resulting `~/profile.zip` as described below.

## Uploading a profile

The archive is a `.zip` of the user-data directory you built above. It can hold cookies and saved logins, so treat it as a secret.

A profile archive can be large, so the bytes are uploaded **directly to object storage** through a presigned URL and never pass through the Agent API. The flow is three steps:

<Steps>
  <Step title="Initiate the upload">
    Call [`POST /initiate-upload`](/computer-use-agents/browser-profiles/initiate-upload). It returns a `profile_id`, a presigned `upload_url`, and the `upload_fields` you must include with the upload.

    <CodeGroup>
      ```bash cURL theme={null}
      INIT=$(curl -s -X POST https://agp.eu.hcompany.ai/api/v2/browser-profiles/initiate-upload \
        -H "Authorization: Bearer $HAI_API_KEY")
      PROFILE_ID=$(echo "$INIT" | jq -r .profile_id)
      UPLOAD_URL=$(echo "$INIT" | jq -r .upload_url)
      ```

      ```python Python theme={null}
      from hai_agents import Client

      client = Client()

      upload = client.browser_profiles.initiate_browser_profile_upload()
      ```

      ```typescript TypeScript theme={null}
      import { HaiAgentsClient } from "hai-agents";

      const client = new HaiAgentsClient();

      const upload = await client.browserProfiles.initiateBrowserProfileUpload();
      ```
    </CodeGroup>
  </Step>

  <Step title="Upload the archive to storage">
    `POST` your profile `.zip` to `upload_url` as `multipart/form-data`, including every entry from `upload_fields`. This request goes to object storage, not to the Agent API. The archive must be `application/zip` and at most **1 GB**; storage rejects anything else.

    <CodeGroup>
      ```bash cURL theme={null}
      args=()
      while IFS= read -r line; do
        args+=(--form-string "$line")
      done <<< "$(echo "$INIT" | jq -r '.upload_fields | to_entries[] | "\(.key)=\(.value)"')"

      curl -sS --fail-with-body -w "upload: HTTP %{http_code}\n" \
        -X POST "$UPLOAD_URL" "${args[@]}" -F "file=@profile.zip"

      # Healthy upload will return `upload: HTTP 204`
      ```

      ```python Python theme={null}
      import requests

      with open("profile.zip", "rb") as f:
          resp = requests.post(
              upload.upload_url,
              data=upload.upload_fields,
              files={"file": ("profile.zip", f, "application/zip")},
          )
          resp.raise_for_status()
      ```

      ```typescript TypeScript theme={null}
      import { readFileSync } from "fs";

      const form = new FormData();
      for (const [key, value] of Object.entries(upload.uploadFields)) {
        form.append(key, value as string);
      }
      form.append("file", new Blob([readFileSync("profile.zip")]), "profile.zip");
      const resp = await fetch(upload.uploadUrl, { method: "POST", body: form });
      if (!resp.ok) throw new Error(`upload failed: ${resp.status}`);
      ```
    </CodeGroup>

    The presigned `upload_url` and `upload_fields` are passed through to object storage exactly as returned, so do not modify them. They expire after `upload_expires_in` seconds, so upload promptly after initiating.
  </Step>

  <Step title="Complete the upload">
    Call [`POST /{profile_id}/complete-upload`](/computer-use-agents/browser-profiles/complete-upload) with the profile metadata (`name`, `browser_name`, `browser_version`) **within 1 day** of uploading. The platform verifies the uploaded archive and creates the profile record. Until you complete it, the upload is held as pending and is deleted automatically after 1 day.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST "https://agp.eu.hcompany.ai/api/v2/browser-profiles/$PROFILE_ID/complete-upload" \
        -H "Authorization: Bearer $HAI_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"name": "acme-prod-login", "browser_name": "chromium", "browser_version": "131"}'
      ```

      ```python Python theme={null}
      profile = client.browser_profiles.complete_browser_profile_upload(
          profile_id=upload.profile_id,
          name="acme-prod-login",
          browser_name="chromium",
          browser_version="131",
      )
      print(profile.id)
      ```

      ```typescript TypeScript theme={null}
      const profile = await client.browserProfiles.completeBrowserProfileUpload({
        profileId: upload.profileId,
        name: "acme-prod-login",
        browserName: "chromium",
        browserVersion: "131",
      });
      console.log(profile.id);
      ```
    </CodeGroup>
  </Step>
</Steps>

Once created, manage your profiles with the [browser profile endpoints](/computer-use-agents/browser-profiles/list): list, retrieve, and delete them.

## Persisting state back into the profile

By default a profile is **read-only**: every session starts from the profile's saved state, and whatever the agent does during the run (new cookies, refreshed tokens, dismissed banners) is discarded when the session ends. Over time the profile's sessions expire and agents are back to login walls.

Set `persist_browser_profile: true` alongside `browser_profile_id` (or `use_default_browser_profile`) on the [Browser environment](/computer-use-agents/browser/configuration) to flip that: when the session ends, the browser's final state is written back into the profile, replacing its previous contents. The next session that loads the profile picks up exactly where the last one left off.

```json Browser that keeps its profile fresh theme={null}
{
  "id": "acme-browser",
  "kind": "web",
  "browser_profile_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "persist_browser_profile": true
}
```

A few rules keep concurrent runs from corrupting each other's state:

* **One writer at a time.** Only one active session may persist a given profile; the session holds an exclusive write lock on the profile until it ends. A second session requesting `persist_browser_profile` on the same profile still starts, but runs **read-only** for that round — check the session's effective persist state if the write-back matters to you. Any number of sessions may load the profile read-only in parallel.
* **Last write wins.** Persisting replaces the profile's entire stored state with the session's final state; it is not merged.
* **Decided at create time.** Persistence can only be requested when the session is created, not when attaching to an existing session.
* **Best-effort on stop.** The write-back happens as the session stops. If it fails, the session still stops cleanly and the profile keeps its previous state.

Sessions that follow your [default profile](#default-profiles) persist automatically — no flag needed; see below.

## Default profiles

Every user has a **default profile** per browser flavor (for example one for `chromium`): a browser that remembers you from session to session, with zero setup. Opt a Browser environment into it with `use_default_browser_profile: true`:

```json Browser that follows the default profile theme={null}
{
  "id": "acme-browser",
  "kind": "web",
  "use_default_browser_profile": true
}
```

* **First use bootstraps it.** If you have no default yet, an empty profile is created and marked as your default automatically when the session starts — no upload, no extra API call. Log in to your sites once (for example through the session's live view) and the state carries forward from then on.
* **Sessions save back automatically.** A session that follows the default profile writes its final browser state (cookies, storage) back when it ends. The write-back is best-effort: when several default-profile sessions run at once, the first to start becomes the writer and the others load the profile read-only for that round — they never fail over it.
* **Explicit persist is best-effort too.** `persist_browser_profile: true` requests the writer role but never blocks the run: if another session already holds the [write lock](#persisting-state-back-into-the-profile), the session starts read-only instead of failing. For a run that must persist (for example a scheduled login-refresh job), verify the session's effective persist state after creation and reschedule if it was downgraded.

`use_default_browser_profile` is mutually exclusive with `browser_profile_id` — name a profile or follow the default, not both. Auto-created defaults appear in your [profile list](/computer-use-agents/browser-profiles/list) with a `default-<browser>-...` name; deleting one simply means your next default-profile session starts over with a fresh empty default.

### Choosing your own default

If you'd rather your agents follow a profile you curated — for example one [uploaded from a real machine](#creating-a-profile) — promote it with [`PUT /browser-profiles/{profile_id}/default`](/computer-use-agents/browser-profiles/set-default); setting a new default for the same browser replaces the previous one. Check what is currently set with [`GET /browser-profiles/default`](/computer-use-agents/browser-profiles/get-default), and clear it with [`DELETE /browser-profiles/{profile_id}/default`](/computer-use-agents/browser-profiles/unset-default). Profile objects report their status in the `is_default` field. You can only mark profiles you created yourself as your default.
