--- title: Logins, credentials, and persistent sessions | Lightcone description: 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: 1. **Persistent sessions.** Sign in once, snapshot the session, reuse it. Best for services you use repeatedly. 2. **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. 3. **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 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 in with 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 in with client.computer.create( kind="browser", environment_id=environment_id, ) as computer: computer.navigate("https://app.example.com/dashboard") result = computer.screenshot() # already authenticated ``` ``` import Lightcone from "@tzafon/lightcone"; const client = new Lightcone(); // One-time setup: create a persistent computer and sign in const 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 snapshot await client.computers.delete(computer.id!); const environmentId = computer.id!; // Every run after that: boot already logged in const 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. Sessions expire on the service’s schedule, not yours. When a saved session’s cookies go stale, repeat the one-time sign-in and save a fresh snapshot. ## Strategy B: human-in-the-loop login handoff For agent [tasks](/guides/tasks/index.md), 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 When no human is in the loop and you can’t pre-bake a session, type credentials via the [Computers API](/guides/computers/index.md) 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 field await client.computers.type(id, { text: process.env.APP_USERNAME! }); await client.computers.click(id, { x: 640, y: 380 }); // password field await 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 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 code result = computer.screenshot() # ... if still on the 2FA screen: computer.type(totp.now()) again ``` ``` import { 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 code ``` ## 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 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: true` on 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 - [**Computers**](/guides/computers/index.md): lifecycle, actions, and persistent state - [**Tasks**](/guides/tasks/index.md): events, configuration, and multi-turn sessions - [**Running in production**](/guides/production/index.md): retries, warm sessions, and cost control - [**Best practices**](/guides/best-practices/index.md): reliability patterns