Logins, credentials, and persistent sessions
Three strategies for working behind a login. persistent sessions, human-in-the-loop handoff, and scripted sign-in.
Most real work happens behind a login: your CRM, your ERP, your bank portal. Getting Northstar (or your own automation) past the sign-in wall is the most common question we get, so here are the three strategies that work, in order of preference:
- Persistent sessions. Sign in once, snapshot the session, reuse it. Best for services you use repeatedly.
- Human-in-the-loop handoff. The agent pauses at the login wall and a human signs in through the live view. Best for one-off access and high-sensitivity credentials.
- Scripted login. Type credentials via the Computers API from your own secret store. Use when the first two don’t fit.
Whichever you choose: don’t put credentials in agent instructions. Instructions pass through the model; they can appear in logs, screenshots of the transcript, and task results.
Strategy A: persistent sessions
Section titled “Strategy A: persistent sessions”Create a computer with persistent: true, sign in once, then delete the computer, which commits the snapshot. New computers booted from that environment_id start already logged in.
For browser sessions, cookies and local storage persist. For desktop sessions, the full disk state persists: installed apps, saved passwords in the browser profile, config files, everything.
from tzafon import Lightcone
client = Lightcone()
# One-time setup: create a persistent computer and sign inwith client.computer.create(kind="browser", persistent=True) as computer: computer.navigate("https://app.example.com/login") print(f"Sign in via the live view: https://lightcone.ai/c/{computer.id}") input("Press Enter once you've signed in...") environment_id = computer.id # Snapshot is committed when the block exits (computer is deleted)
# Every run after that: boot already logged inwith client.computer.create( kind="browser", environment_id=environment_id,) as computer: computer.navigate("https://app.example.com/dashboard") result = computer.screenshot() # already authenticatedimport Lightcone from "@tzafon/lightcone";
const client = new Lightcone();
// One-time setup: create a persistent computer and sign inconst computer = await client.computers.create({ kind: "browser", persistent: true,});await client.computers.navigate(computer.id!, { url: "https://app.example.com/login",});console.log(`Sign in via the live view: https://lightcone.ai/c/${computer.id}`);// ... wait for the human to sign in, or script the login (Strategy C) ...
// Deleting the computer commits the snapshotawait client.computers.delete(computer.id!);const environmentId = computer.id!;
// Every run after that: boot already logged inconst restored = await client.computers.create({ kind: "browser", environment_id: environmentId,});You can do the one-time sign-in yourself through the live view (easiest), or script it (Strategy C). Either way, subsequent runs never touch credentials.
Strategy B: human-in-the-loop login handoff
Section titled “Strategy B: human-in-the-loop login handoff”For agent tasks, pass enable_login_handoff: true. When Northstar hits a sign-in wall, it emits a login_required event with a screenshot and pauses the task. A human opens the live view at lightcone.ai/c/{computer_id}, signs in directly on the computer, and the task resumes.
for event in client.agent.tasks.start_stream( instruction="Log my hours for this week in the timesheet app", kind="browser", start_url="https://timesheets.example.com", enable_login_handoff=True,): if event.type == "login_required": print(f"Sign-in needed: {event.screenshot}") print(f"Take over at: https://lightcone.ai/c/{event.computer_id}") # Notify your on-call human; the task stays paused until # they sign in and the task is resumed. elif event.type == "completed": print(event.result)const stream = await client.agent.tasks.startStream({ instruction: "Log my hours for this week in the timesheet app", kind: "browser", start_url: "https://timesheets.example.com", enable_login_handoff: true,});
for await (const event of stream) { if (event.type === "login_required") { console.log(`Sign-in needed: ${event.screenshot}`); console.log(`Take over at: https://lightcone.ai/c/${event.computer_id}`); } else if (event.type === "completed") { console.log(event.result); }}The security property is the point: credentials are entered by the human directly into the session. They never pass through the model, the task instruction, or your application code. The agent only ever sees the post-login screen.
Combine with persistent: true and the human only has to do this once per session expiry; subsequent tasks restore the logged-in state.
Strategy C: scripted login
Section titled “Strategy C: scripted login”When no human is in the loop and you can’t pre-bake a session, type credentials via the Computers API from your own secret store:
import os
with client.computer.create(kind="browser", persistent=True) as computer: computer.navigate("https://app.example.com/login") computer.wait(2)
result = computer.screenshot() # find the field coordinates computer.click(640, 320) # username field computer.type(os.environ["APP_USERNAME"]) computer.click(640, 380) # password field computer.type(os.environ["APP_PASSWORD"]) computer.hotkey("enter") computer.wait(3)const id = computer.id!;await client.computers.navigate(id, { url: "https://app.example.com/login" });
await client.computers.click(id, { x: 640, y: 320 }); // username fieldawait client.computers.type(id, { text: process.env.APP_USERNAME! });await client.computers.click(id, { x: 640, y: 380 }); // password fieldawait client.computers.type(id, { text: process.env.APP_PASSWORD! });await client.computers.hotkey(id, { keys: ["enter"] });Credentials come from your environment or secret manager and go straight to the computer as keystrokes, never into an agent instruction.
TOTP / 2FA
Section titled “TOTP / 2FA”Compute the one-time code locally and type it. Never give the TOTP seed to the model.
import pyotp
totp = pyotp.TOTP(os.environ["APP_TOTP_SECRET"])computer.type(totp.now())computer.hotkey("enter")computer.wait(2)
# If the 30-second window expired mid-entry, retry with a fresh coderesult = computer.screenshot()# ... if still on the 2FA screen: computer.type(totp.now()) againimport { authenticator } from "otplib";
const code = authenticator.generate(process.env.APP_TOTP_SECRET!);await client.computers.type(id, { text: code });await client.computers.hotkey(id, { keys: ["enter"] });// If the 30-second window expired mid-entry, generate and type a fresh codeSession reuse for multi-turn work
Section titled “Session reuse for multi-turn work”Tasks can reuse sessions across turns. Pass computer_id on a task request to attach to a live computer, or restore a saved one with the same ID. Control the fallback with on_missing_computer:
| Value | Behavior when computer_id is not live |
|---|---|
"restore" (default) | Restore a saved persistent session with that ID, or return 404 |
"fail" | Always return 404 |
"create_new" | Restore when possible, otherwise start a fresh computer |
client.agent.tasks.start( instruction="Now export that report as CSV", kind="browser", computer_id=saved_computer_id, on_missing_computer="restore",)await client.agent.tasks.start({ instruction: "Now export that report as CSV", kind: "browser", computer_id: savedComputerId, on_missing_computer: "restore",});Between turns, an idle desktop can be parked; you see a computer_parked event before it is snapshotted. When the next turn arrives, the session is restored in under 2 seconds, logged-in state intact, so multi-turn conversations don’t pay for idle compute.
Anti-bot measures and captchas
Section titled “Anti-bot measures and captchas”Some sites treat any automation as hostile. Two things reduce friction:
- Stealth-mode browser environments. Browser computers are configured to look like ordinary browsers.
- Residential proxies. Pass
use_advanced_proxy: trueon browser computers for sites with aggressive bot detection.
with client.computer.create(kind="browser", use_advanced_proxy=True) as computer: computer.navigate("https://protected-site.com")const computer = await client.computers.create({ kind: "browser", use_advanced_proxy: true,});These help, but they are not a guarantee. Captchas and hard bot walls may still appear. The reliable answer is the human-handoff pattern from Strategy B, not evasion. And as always: check a site’s terms of service before automating it.
See also
Section titled “See also”- Computers: lifecycle, actions, and persistent state
- Tasks: events, configuration, and multi-turn sessions
- Running in production: retries, warm sessions, and cost control
- Best practices: reliability patterns