Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 9 min read

Browser Agent Handoff: Let Users Take Over Without Losing Task State

Browser agents look impressive until a real task hits a login screen, an MFA prompt, a CAPTCHA, or a page that needs a human decision. The weak fix is to stop the run and ask the user to start over. The risky fix is to l

Browser agents look impressive until a real task hits a login screen, an MFA prompt, a CAPTCHA, or a page that needs a human decision. The weak fix is to stop the run and ask the user to start over. The risky fix is to let the agent keep control while the user enters a password.

There is a better boundary: pause automation, grant the person exclusive control of one browser surface, preserve only the task state needed to resume, and make the return to the agent explicit and narrow. This article shows how to build that browser agent handoff pattern without passing secrets through an LLM context or treating β€œthe user clicked Continue” as proof that the task is safe.

The goal is not to make agents log in for people. It is to make the handoff predictable when only a person can complete the next step.

What a browser agent handoff actually is

A handoff is a short-lived change in who may operate a specific browser session. The agent pauses, a user completes an interaction in the browser UI, and the agent later resumes from a validated checkpoint.

That is different from a chat approval. A chat approval says, β€œI agree in principle.” A browser handoff says, β€œI have control of this exact tab for this bounded purpose.” It should be used for:

  • sign-in, passkeys, MFA, CAPTCHA, and device approval;
  • reviewing a prefilled form before submission;
  • resolving an ambiguous choice that requires account context;
  • completing a payment or accepting a legal or security notice;
  • correcting a page state the agent cannot safely infer.

Treat the browser as a capability, not a text window. An agent can observe page text and still be wrong about what a button does. A user can finish a login and still not want the agent to submit the next irreversible form.

The hidden failure: a pause is not a handoff

Many implementations pause the agent with a boolean such as waiting_for_user. That loses the details that make resumption safe. Which tab is locked? What operation was planned? Has the user changed the page? Did the session expire? Is the final action still approved?

Instead, model the handoff as a state machine:

RUNNING -> HANDOFF_REQUESTED -> HUMAN_CONTROLLED -> RESUME_PENDING
  ^                                                        |
  |---------------- VALIDATED_RESUME <--------------------|
                         |
                         +--> EXPIRED / ABORTED

Only one actor owns the control lease at a time. While a person has the lease, the agent must not click, type, read new page content, or run page JavaScript. That last restriction matters: an agent-side helper running during a password or MFA step can turn a supposedly private handoff into credential exposure.

Define a handoff contract before opening the browser

Do not let the agent improvise a handoff from arbitrary page text. Give the runtime a typed request that the UI and policy layer can evaluate.

type HandoffKind = "login" | "mfa" | "captcha" | "review" | "payment" | "choice";

type HandoffRequest = {
  runId: string;
  tabId: string;
  kind: HandoffKind;
  reason: string;                 // displayed to the person
  allowedOrigin: string;          // exact origin, not a substring match
  resumeIntent: "inspect" | "continue-draft" | "submit-after-approval";
  expectedSignals: Array<"origin" | "auth-state" | "url-pattern" | "dom-marker">;
  expiresAt: string;
  operationId: string;
};

The contract gives the user a plain-language reason, but it also gives the runtime enforceable boundaries. A request for https://billing.example.com must not quietly become a handoff on https://billing-example.com. A request to sign in should not silently authorize a final purchase.

Keep reason generated from a constrained template or reviewed policy data. Never display raw instructions from the current page as the reason for a security-sensitive action; web content is untrusted input.

Create an exclusive control lease

When the policy layer accepts a request, the browser broker issues a one-time lease. Bind it to the run, tab, origin, and intended action. Store the server-side record; return only an opaque token to the user interface.

const lease = await handoffs.create({
  runId,
  tabId,
  origin: "https://portal.example.com",
  kind: "mfa",
  expiresInSeconds: 300,
  resumeIntent: "continue-draft",
});

await browser.pauseAutomation(tabId, { leaseId: lease.id });
await audit.append({ type: "handoff_started", runId, tabId, leaseId: lease.id });

During the lease, reject every agent browser tool call for that tab. Do not merely queue clicks: queued commands can become dangerous when the user returns control on an unexpected screen. If a background browser process exists, pause its navigation and automation hooks too.

A useful UI shows the origin, the reason, time remaining, and a clear Return to agent control. It should not show the agent’s whole chain of thought or invite the user to paste secrets into chat. The person interacts with the real browser, using their normal password manager or security key.

Preserve a receipt, not the browser’s secrets

The agent needs enough information to decide what to do next. It does not need the password, MFA code, cookies, or a raw screenshot of an account page.

Create a redacted handoff receipt before control changes hands:

{
  "operationId": "op_8b5",
  "tabId": "tab_41",
  "origin": "https://portal.example.com",
  "urlPath": "/signin",
  "intent": "download March invoices",
  "nextSafeAction": "inspect authenticated landing page",
  "blockedAction": "download files until user confirms scope",
  "createdAt": "2026-11-15T10:12:00Z"
}

On return, record a second receipt with observations collected by trusted browser code, such as the final origin, a route category, and an explicit userReturnedControl event. Keep query strings, cookie values, form fields, one-time codes, page bodies, and screenshots out of the default receipt. If screenshots are needed for debugging, make them opt-in, encrypted, access-controlled, and separately retained.

This separation also improves incident review. You can answer β€œwhy did the agent resume?” without storing β€œwhat did the user type?”

Validate the resume instead of trusting it

When the user presses Return, the broker must not immediately unpause the agent. First compare the actual browser state with the contract.

const observed = await browser.inspectTrusted(tabId, {
  fields: ["origin", "urlPath", "authState", "pageMarker"],
});

const valid =
  observed.origin === lease.origin &&
  observed.authState === "authenticated" &&
  matchesAllowedRoute(observed.urlPath, lease.resumeIntent) &&
  new Date() < lease.expiresAt;

if (!valid) {
  await handoffs.close(lease.id, "resume_validation_failed");
  return requestFreshHandoff(runId, "The page changed; please review the new state.");
}

Use a trusted browser-side capability for this inspection. Do not ask the model, β€œDoes the page look logged in?” The page may contain prompt injection, visual ambiguity, or a fake success banner. The model may summarize the page, but it should not be the authority that transfers control.

For high-impact actions, validation should lead to another explicit gate. For example, a login handoff may permit the agent to gather a draft, while a separate review handoff is required before submission. This prevents authentication from becoming an accidental blanket approval.

Separate authentication, authorization, and submission

These three events are easy to blur in a demo and expensive to blur in production.

Event Who should decide? What the agent may do afterward
Authentication Person Resume a bounded, non-destructive task
Authorization scope Policy + person Use only permitted site actions and data
Irreversible submission Person, unless pre-approved policy allows it Execute exactly one named side effect

Say an agent is preparing expense entries. The user may log in, the policy may allow the agent to read transactions and fill a draft, and the user may still need to review before β€œSubmit.” Store those as separate operation IDs and receipts. A valid login must never be reused as approval for a new recipient, changed amount, or different site.

Handle expiry, tab changes, and uncertain outcomes

Real browser sessions are messy. A user may close a tab, navigate to a different origin, leave the handoff overnight, or finish a form right as the network fails. Build recovery paths rather than guessing.

  • Lease expired: revoke agent control and require a new request.
  • Tab closed: mark the operation abandoned; do not open a replacement tab with the old permission.
  • Origin changed: stop and explain the mismatch. A redirect may be legitimate, but it needs a new allowlist decision.
  • Submission uncertain: record an unknown operation state and reconcile by reading a safe confirmation source. Never retry a payment, deletion, or form submission blindly.
  • User cancels: discard queued plans and leave the browser under user control or close only agent-created tabs according to your cleanup policy.

Idempotency keys help when a site supports them. Where they do not, capture a non-sensitive confirmation reference and turn retries into a reconciliation workflow. β€œIt probably did not send” is not a safe state.

Test the handoff like a security boundary

Happy-path tests are not enough. Use a mock site and test the broker, extension, and orchestration layer together.

it("does not let a login lease authorize a final submit", async () => {
  const lease = await startHandoff({ kind: "login", resumeIntent: "continue-draft" });
  await humanCompletesLogin(lease);
  await returnControl(lease);

  await expect(agent.click("button[type=submit]")).rejects.toThrow(
    "fresh approval required"
  );
});

Add cases for a look-alike origin, expired lease, page navigation during control, agent tool calls during control, a malicious page that asks for a file upload, and a resume token used twice. Log the policy decision and result for each case, but redact the page data. These tests catch the shortcut where a UI looks controlled while a background automation channel still has power.

A rollout checklist for small teams

Start with the narrowest useful version:

  1. Support login and MFA only, on an explicit origin allowlist.
  2. Make human control exclusive and expiring.
  3. Use opaque one-time resume tokens stored server-side.
  4. Persist redacted start and return receipts.
  5. Require a fresh approval for final submission.
  6. Add metrics: handoff rate, expiry rate, resume-validation failure rate, and uncertain-outcome rate.
  7. Review a sample of failures to improve the task policy, not to capture more private browser data.

The strongest browser agent systems do not eliminate people from sensitive moments. They make the boundary visible, short-lived, and auditable. That lets the agent continue useful work after a login without quietly inheriting a person’s full browser authority.

Measure whether handoffs are helping

Track the handoff as an operational event, not a mysterious interruption. Measure the percentage of runs that require human control by site and task type, then pair it with the percentage that resume successfully. A high handoff rate may reveal a brittle navigation flow; a high validation-failure rate often points to redirects or an over-broad resume rule.

Keep the metrics coarse and privacy-safe. You need counts, durations, outcome categories, policy IDs, and redacted origin labelsβ€”not passwords, form values, or the contents of private pages. Split outcomes into resumed, expired, cancelled, validation_failed, and unknown_side_effect. Review the last category first. It is where blind retry logic and ambiguous final actions tend to hide.

One practical alert is a sudden increase in handoffs for a single origin. It can mean a site changed its login journey, a session cookie policy changed, or an adversarial page is steering agents toward a sensitive screen. Pausing that origin’s automation until a person reviews the policy is safer than teaching the agent to push through.

FAQ

Should an agent ever see a password or MFA code?

No. The user should enter credentials in the browser’s normal UI. The agent receives only a validated, redacted result such as β€œauthenticated at the allowed origin,” never the secret itself.

Is a chat confirmation enough to resume a browser agent?

Not for sensitive workflows. Chat is useful for intent, but the runtime should require a scoped browser control lease and a validated return state before it resumes automation.

How long should a browser agent handoff lease last?

Use the shortest window that works, often a few minutes for a login or MFA step. Expired leases should require a fresh handoff rather than being silently extended.

Can the agent keep reading the page while the user is in control?

It should not. Pause agent inspection, screenshots, DOM extraction, scripts, and queued actions for the leased tab. This preserves the meaning of private human control.

What happens if the user finishes a submission during the handoff?

Treat the result as uncertain until a trusted confirmation check records a safe receipt. Do not submit again automatically; reconcile using a confirmation ID or a read-only status view.

Does a successful login authorize later actions on the same site?

No. Authentication proves account access, not approval for every action. Scope subsequent agent actions in policy, and put a separate gate in front of destructive or externally visible submissions.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.