> ## 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.

# Handle two-factor authentication

> When a login or signup asks for a one-time password or confirmation link, let the agent request it through a prebuilt custom tool.

export const TwoFactorAuth = () => {
  const Chip = ({children}) => <span className="rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-[11px] text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
      {children}
    </span>;
  const ArrowRight = () => <svg className="h-4 w-8 text-zinc-400 dark:text-zinc-500" viewBox="0 0 32 16" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"><path d="M1 8h27" /><path d="M23 3l6 5-6 5" /></svg>;
  const ArrowLeft = () => <svg className="h-4 w-8 text-zinc-400 dark:text-zinc-500" viewBox="0 0 32 16" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"><path d="M31 8H4" /><path d="M9 3 3 8l6 5" /></svg>;
  return <div className="not-prose my-6 overflow-x-auto">
      <div className="flex items-stretch justify-center">
        <div className="flex w-[210px] shrink-0 flex-col rounded-xl border border-transparent bg-zinc-900 p-4 dark:bg-zinc-100">
          <div className="flex items-center gap-2 text-[13.5px] font-semibold text-white dark:text-zinc-900">
            <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2" /><path d="M20 14h2" /><path d="M15 13v2" /><path d="M9 13v2" /></svg>
            <span>Agent</span>
          </div>
          <div className="mt-1 text-[11.5px] leading-4 text-zinc-400 dark:text-zinc-500">login asks for a code</div>
          <div className="mt-3">
            <span className="rounded-md bg-zinc-800 px-2 py-0.5 font-mono text-[10.5px] text-zinc-300 dark:bg-zinc-200 dark:text-zinc-600">request_otp</span>
          </div>
          <div className="mt-auto pt-4 text-[11px] leading-4 text-zinc-400 dark:text-zinc-500">never guesses a code; waits for one</div>
        </div>

        <div className="flex shrink-0 flex-col items-center justify-center gap-4 px-3">
          <div className="flex flex-col items-center gap-1">
            <span className="whitespace-nowrap text-[11px] text-zinc-500 dark:text-zinc-400">needs OTP</span>
            <Chip>prompt · kind · source</Chip>
            <ArrowRight />
          </div>
          <div className="flex flex-col items-center gap-1">
            <ArrowLeft />
            <Chip>code or link</Chip>
            <span className="whitespace-nowrap text-[11px] text-zinc-400 dark:text-zinc-500">single value only</span>
          </div>
        </div>

        <div className="flex w-[210px] shrink-0 flex-col rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-950">
          <div className="flex items-center gap-2 text-[13.5px] font-medium text-zinc-900 dark:text-zinc-100">
            <span className="text-zinc-500 dark:text-zinc-400">
              <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
            </span>
            <span>Your handler</span>
          </div>
          <div className="mt-3 flex flex-wrap gap-2">
            {["stdin", "IMAP inbox", "custom"].map(t => <span key={t} className="rounded-lg border border-zinc-200 bg-zinc-50 px-2.5 py-1 font-mono text-[11px] text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300">{t}</span>)}
          </div>
          <div className="mt-auto pt-3 text-[11px] leading-4 text-zinc-400 dark:text-zinc-500">runs locally; inbox stays on your machine</div>
        </div>
      </div>
    </div>;
};

<TwoFactorAuth />

Sites that protect a login with email codes, SMS codes, or confirmation links send a value the agent cannot invent. The SDKs ship a prebuilt [custom tool](/computer-use-agents/custom-tools) for that moment: the agent calls `request_otp`, your process resolves the code or link, and the run continues with the single value.

Pass `otp_tool` / `otpTool` in `tools` the same way you would any other custom tool. Without a handler it prompts on stdin; with a handler you can read an inbox over IMAP or fetch the value from anywhere else your code can reach.

## Prompt interactively

The default handler is enough for local runs: when the agent hits a 2FA step, your terminal asks for the code or link.

<CodeGroup>
  ```python Python theme={null}
  from hai_agents import Client
  from hai_agents_tools import otp_tool

  client = Client()
  result = client.run_session(
      agent="h/web-surfer-flash",
      messages="Log in to example.com and summarize the inbox.",
      tools=[otp_tool()],
  )
  print(result.answer)
  ```

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

  const client = new HaiAgentsClient();
  const result = await client.runSession({
    agent: "h/web-surfer-flash",
    messages: "Log in to example.com and summarize the inbox.",
    tools: [otpTool()],
  });
  console.log(result.answer);
  ```
</CodeGroup>

## Read the code from email

For unattended runs, hand the tool an IMAP handler. It polls unread mail (newest first), extracts a code or confirmation link, marks that message read so a retry cannot reuse a stale code, and returns only that value to the agent. For Gmail or Google Workspace, use an [app password](https://support.google.com/accounts/answer/185833).

<CodeGroup>
  ```python Python theme={null}
  import os

  from hai_agents import Client
  from hai_agents_tools import imap_otp_handler, otp_tool

  handler = imap_otp_handler(
      host="imap.gmail.com",
      username="agent-inbox@gmail.com",
      password=os.environ["GMAIL_APP_PASSWORD"],
      sender="no-reply@example.com",  # optional: only mail from this address
  )

  client = Client()
  result = client.run_session(
      agent="h/web-surfer-flash",
      messages="Log in to example.com and check for new notifications.",
      tools=[otp_tool(handler)],
  )
  print(result.answer)
  ```

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

  // Optional deps for the IMAP handler: npm install imapflow mailparser
  const handler = imapOtpHandler({
    host: "imap.gmail.com",
    username: "agent-inbox@gmail.com",
    password: process.env.GMAIL_APP_PASSWORD!,
    sender: "no-reply@example.com", // optional: only mail from this address
  });

  const client = new HaiAgentsClient();
  const result = await client.runSession({
    agent: "h/web-surfer-flash",
    messages: "Log in to example.com and check for new notifications.",
    tools: [otpTool({ handler })],
  });
  console.log(result.answer);
  ```
</CodeGroup>

Like every custom tool, the handler runs in your process: IMAP credentials never leave your machine, and the agent only receives the extracted code or link — never mailbox contents, subjects, or senders.

Useful IMAP options:

| Option                         | Default             | Role                                                                      |
| ------------------------------ | ------------------- | ------------------------------------------------------------------------- |
| `sender`                       | unset               | Only consider messages from this address                                  |
| `timeout_s` / `timeoutMs`      | 2 minutes           | Give up (tool error to the agent) after this long                         |
| `max_age_s` / `maxAgeMs`       | 15 minutes          | Ignore unread mail older than this                                        |
| `code_pattern` / `codePattern` | built-in heuristics | Override extraction; first capture group (or the whole match) is the code |

## Supply a custom handler

Any function that takes the agent's request and returns a string works: prompt in Slack, call an inbox API, read SMS from a provider, and so on. Handlers may be sync or async.

<CodeGroup>
  ```python Python theme={null}
  from hai_agents_tools import OtpRequest, otp_tool

  def from_slack(request: OtpRequest) -> str:
      # request.prompt, request.kind ("code" | "link"), request.source
      return slack.ask_user(request.prompt)

  tools = [otp_tool(from_slack)]
  ```

  ```typescript TypeScript theme={null}
  import { otpTool, type OtpRequest } from "hai-agents";

  async function fromSlack(request: OtpRequest): Promise<string> {
    // request.prompt, request.kind ("code" | "link"), request.source
    return slack.askUser(request.prompt);
  }

  const tools = [otpTool({ handler: fromSlack })];
  ```
</CodeGroup>

## What the agent sends

The tool's input schema is fixed. The agent fills:

| Field    | Required | Meaning                                                                       |
| -------- | -------- | ----------------------------------------------------------------------------- |
| `prompt` | yes      | Human-readable ask, e.g. "Enter the 6-digit code sent to j\*\*\*@example.com" |
| `kind`   | no       | `"code"` (default) or `"link"` for a full confirmation URL                    |
| `source` | no       | Where it was sent, e.g. `"email"`, `"sms"`, `"authenticator app"`             |

Your handler should return a non-empty string. Empty values fail as a tool error so the agent can retry or stop cleanly.

## Authenticator apps via a vault

If the site uses a TOTP authenticator and the secret already lives in [1Password](https://developer.1password.com/), bind a [vault](/computer-use-agents/vaults/overview) to the browser instead. When the page matches a stored item, the session offers [`fill_secret_at`](/computer-use-agents/browser/configuration#actions) with `totp` and injects the code without putting it in the agent's context. Reach for `otp_tool` when the code arrives out of band (email, SMS, magic link); reach for a vault when the TOTP secret is already in your secrets provider.
