Skip to content
Try out in chatDeveloper dashboardLogin

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:

  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.

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.

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.

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

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

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:

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

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.