Getting started with Nightjar
Who this is for. You have an AI agent (Claude Code, Claude Desktop, Codex, Gemini, a custom SDK agent) — or just a terminal — and you want it to drive a real browser. Maybe against a public site, maybe against the app you’re running on your own laptop. This guide tells you exactly what to do in your environment, whatever that is.
Honesty up front. Part 1 (drive a public site) works today with zero install. Part 2 (reach your own
localhost) works today but is rough — the smooth one-command version is designed in ADR-035 — Local Reach Grants and not built yet. Every step below is labelled Today or Target so you never follow an instruction for something that doesn’t exist.
The mental model (read this first — it removes 90% of the confusion)
Nightjar gives your agent a browser running in our cloud. Your agent talks to it over MCP (an HTTPS tool endpoint). That’s the whole product. Everything else is detail hanging off two independent questions plus one rule:
┌─────────────────────────┐ HTTPS / MCP ┌──────────────────────────┐
YOU ───► │ your AGENT HOST │ ───────────────────────► │ Nightjar CLOUD BROWSER │
│ (Claude Code / Desktop, │ session_start, │ (runs in our datacenter) │
│ Codex, Gemini, SDK, │ navigate, perceive… │ │
│ or a plain terminal) │ ◄─────────────────────── │ │
└─────────────────────────┘ └──────────┬────────────────┘
│
┌─────────────────────────────┴───────────────┐
│ │
a PUBLIC site YOUR localhost / LAN
(nothing to install) (needs a daemon on YOUR machine) - Question A — where does your agent run? (the “host”) This decides how you connect the MCP and get a token. It’s the same answer whether you’re on Windows, macOS, or Linux — it depends on the agent, not the OS.
- Question B — what OS are you on? This only matters for installing the
nightjarCLI/daemon, and you only need that for local reach (Part 2). For driving public sites (Part 1), the OS is irrelevant — you install nothing. - The one rule: the cloud browser’s
localhostis our datacenter, not your machine. To make a session reach your app, a small daemon must run on the same machine as that app, and tunnel the traffic back. If your agent already runs on that machine (the common case), this is trivial. If your agent runs somewhere else (a cloud agent, a CI runner), the daemon still goes on your machine — see the co-location rule.
Keep these separate and the rest is mechanical.
Part 1 — Drive a cloud browser against a public site (Today, no install)
This is the two-call delight. You need (1) an org and a token, and (2) your host pointed at the Nightjar MCP endpoint:
https://api.nightjar.cloud/mcp It’s a streamable-HTTP MCP server, authenticated with Authorization: Bearer <token>. The endpoint is open on the public internet
(alpha access is gated by org membership + your token, not by your IP) — so
it works the same from a laptop, a server, or a cloud sandbox.
Step 1 — Get a token
Two ways, pick whichever your host makes easy:
- Paste a static key (works everywhere). Log in to
console.nightjar.cloudwith GitHub, land in your org, and mint an API key (njk_…). Alpha is invite-gated by org membership: a logged-in user with no org can’t do anything, so if the console is empty, you need an invite. - OAuth, no key to copy (smoothest). Hosts that support remote-MCP OAuth
(Claude Code, Codex, Gemini) will discover Nightjar’s OAuth metadata at
https://api.nightjar.cloud/.well-known/oauth-*and run a device/PKCE login in your browser — you never hand-copy a secret. Nightjar supports Dynamic Client Registration (RFC 7591), so even a client we’ve never seen can self-register and complete the flow. (If your host prompts you to “log in” to the Nightjar MCP server, that’s this path.)
Step 2 — Point your host at the MCP endpoint
Find your host below. The OS doesn’t change these — a macOS Claude Code and a Windows Claude Code use the identical command.
Claude Code (CLI and the VS Code extension) — Today
export NIGHTJAR_API_KEY="njk_your_token_here"
claude mcp add --scope user --transport http nightjar https://api.nightjar.cloud/mcp
--header "Authorization: Bearer $NIGHTJAR_API_KEY" Or by config — user-global ~/.claude.json, or per-repo .mcp.json (shareable
with your team):
{
"mcpServers": {
"nightjar": {
"type": "http",
"url": "https://api.nightjar.cloud/mcp",
"headers": { "Authorization": "Bearer ${NIGHTJAR_API_KEY}" }
}
}
} The VS Code extension reads the same config — no separate setup.
Claude Desktop (macOS / Windows) — Today
Remote MCP is added through the UI, not a JSON file:
Settings → Connectors → Add custom connector →
https://api.nightjar.cloud/mcp
The connector UI is OAuth-first, so the cleanest path is the OAuth login from
Step 1. If you must use a static njk_ key and the UI won’t take a header, fall
back to the local mcp-remote bridge in claude_desktop_config.json (%APPDATA%\Claude\claude_desktop_config.json on Windows, ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"nightjar": {
"command": "npx",
"args": ["mcp-remote", "https://api.nightjar.cloud/mcp",
"--header", "Authorization: Bearer ${NIGHTJAR_API_KEY}"],
"env": { "NIGHTJAR_API_KEY": "njk_your_token_here" }
}
}
} (The bridge needs Node 18+. Prefer the OAuth connector when you can.)
Codex CLI (any OS) — Today
export NIGHTJAR_API_KEY="njk_your_token_here"
codex mcp add nightjar --url https://api.nightjar.cloud/mcp
--bearer-token-env-var NIGHTJAR_API_KEY Or in ~/.codex/config.toml:
[mcp_servers.nightjar]
url = "https://api.nightjar.cloud/mcp"
bearer_token_env_var = "NIGHTJAR_API_KEY" For the OAuth path instead of a static key: codex mcp login nightjar. Verify
with /mcp inside a Codex session.
Gemini CLI (any OS) — Today
gemini mcp add nightjar --transport http https://api.nightjar.cloud/mcp
-H "Authorization: Bearer $NIGHTJAR_API_KEY" Or in ~/.gemini/settings.json:
{
"mcpServers": {
"nightjar": {
"httpUrl": "https://api.nightjar.cloud/mcp",
"headers": { "Authorization": "Bearer $NIGHTJAR_API_KEY" }
}
}
} Gemini also supports OAuth discovery — if you omit the header and the server
returns 401, it will discover the OAuth endpoints and run the flow for you.
Verify with /mcp.
Custom SDK agent (TypeScript / Python) — Today
The Claude Agent SDK takes the same MCP shape:
options = ClaudeAgentOptions(
mcp_servers={
"nightjar": {
"type": "http",
"url": "https://api.nightjar.cloud/mcp",
"headers": {"Authorization": f"Bearer {os.environ['NIGHTJAR_API_KEY']}"},
}
},
allowed_tools=["mcp__nightjar__*"],
) Plain terminal, no agent at all (any OS) — Today
There’s no agent to configure — you are the driver. Two options:
- Just the HTTP API + your key —
curl https://api.nightjar.cloud/v1/...withAuthorization: Bearer njk_…. No install. - The
nightjarCLI for ergonomic session/network commands — but note the CLI is currently build-from-source only (see Part 2’s install note); for pure cloud-drive you usually don’t need it.
Step 3 — Drive
Once connected, your agent calls session_start → navigate → perceive (plus act, screenshot, session_end). Reaching a public URL is a two-call
job; the agent is resolved from your token, so you never pass it as an argument.
So, the four examples you asked about
| Your environment | What you do |
|---|---|
| VS Code on Windows (Claude Code extension) | Claude Code recipe above — claude mcp add … --transport http. |
| Claude on a Mac (Desktop or Code) | Claude Desktop connector, or the Claude Code recipe. |
| Codex on Linux | Codex recipe — codex mcp add / ~/.codex/config.toml. |
| Plain CLI on a Mac | The HTTP API + key (or the SDK); no agent host to wire up. |
They’re all “easy” for the same reason: driving the cloud browser needs no local install — you register a remote MCP URL and supply a token. The OS never entered into it.
Part 2 — Reach your own localhost (Today: rough · Target: one command)
The cloud browser can’t see your machine by default — its localhost is our
datacenter. To let a session reach the app you’re running on port 5173 (or your
LAN), you run the nightjar-daemon on that machine. It joins an org-scoped network over wss://tunnel.nightjar.cloud/ws/control, and your session opens
with egress: "split" (public traffic goes direct; only declared local targets
route through your daemon) or egress: "local" (everything through your machine).
Today — the honest, ~7-step path
- Install from source. There are no binaries yet — no
brew, nocurl | sh, nowinget, nonpx. You build the CLI and daemon withcargo build. (This is exactly what ADR-035 Stage 1 is about fixing.) - Create a network, mint a join token, start the daemon with the
network + token + the control URL (
wss://tunnel.nightjar.cloud/ws/control), then open a session withegress: "local"/"split".
The full, tested recipe — including the gotchas — lives in local-server-via-nightjar-daemon.md.
Known sharp edges (don’t be surprised):
- The recommended per-developer OAuth login is scope-clamped and can’t create a
network (that needs
networks:write), so today this path leans on an org/admin key. egress: "local"with no daemon connected fails silently — the session opens fine and only errors later, atnavigate, with a tunnel502.- The CLI can’t always derive the control URL, so you may have to pass the
“magic string”
wss://tunnel.nightjar.cloud/ws/controlby hand.
Target — what this becomes (ADR-035, proposed)
One command, on the machine running your app:
$ nightjar tunnel 5173
Nightjar 0.9.3 verified
Logged in as you@example.com in org acme-dev
Grant: rg_7f3a Allows: http://127.0.0.1:5173, localhost:5173
Egress: split Expires: 18:30 (or Ctrl-C) Daemon: your-laptop connected
Ready for agents: session_start({ "egress": "split", "network": "rg_7f3a" }) It logs you in if needed, mints a narrow, expiring Reach Grant (“this
session may reach this local port through this daemon for this long”), runs
the daemon, and prints a ready-to-use session — defaulting to split so a
single-port verify never silently routes all your browsing through your laptop.
The architecture (daemon = trusted local courier; grant = the user-facing
object; signed install; /.well-known/nightjar discovery; a typed E_LOCAL_REACH_NOT_READY instead of the silent dead-end) is specified in ADR-035 — Local Reach Grants.
This getting-started guide is the user’s-eye view that ADR-035’s Stages 1–2 exist
to deliver.
The co-location rule (the thing everyone trips on)
localhost resolves on the daemon’s machine — not where your agent runs,
and not in the cloud browser.
Agent runs on the same machine as your app (all four examples above, the common case): run the daemon there too. Same box, done.
Agent runs elsewhere (a cloud-hosted agent, a CI runner, a remote sandbox) but the app is on your laptop: the daemon goes on your laptop. The agent can’t start it for you — so the Target design hands the agent a typed error it can act on:
{ "code": "E_LOCAL_REACH_NOT_READY", "remedy": { "command": "nightjar tunnel 5173 --claim njrg_3p9x --org acme-dev", "authorizes": ["127.0.0.1:5173", "localhost:5173"], "scope": "this agent/session only" } }You run that one command on your laptop; the session becomes ready. (This is ADR-035 Stage 3 — a reach capability the driving identity can self-serve, without handing it org-network admin power. Not built yet.)
Quick reference
| Host | Install needed for cloud-drive? | Connect (Today) | Token |
|---|---|---|---|
| Claude Code (CLI/VS Code) | No | claude mcp add --transport http nightjar <url> --header … | njk_ or OAuth |
| Claude Desktop | No | Settings → Connectors → Add custom connector → <url> (or mcp-remote bridge) | OAuth (preferred) / njk_ |
| Codex CLI | No | codex mcp add / ~/.codex/config.toml (bearer_token_env_var) | njk_ or codex mcp login |
| Gemini CLI | No | gemini mcp add -H / ~/.gemini/settings.json (httpUrl) | njk_ or OAuth discovery |
| SDK (TS/Python) | No | mcpServers: { type: http, url, headers } | njk_ |
| Plain terminal | No | curl …/v1 + Bearer, or the SDK | njk_ |
<url> = https://api.nightjar.cloud/mcp everywhere.
Local reach (Part 2) is the only thing that needs an install, and that
install is build-from-source today → signed brew/curl/winget/npx under ADR-035 Stage 1.
What works today vs. what’s proposed
Works today
- Hosted MCP drive from any remote-MCP host (Claude Code, Codex, Gemini, SDK) +
Claude Desktop (connector/bridge), with
njk_keys or OAuth (incl. DCR self-registration). - The local-reach tunnel itself — daemon, networks, join tokens,
egressmodes, the declared-host allowlist + SSRF floor — built and running (just build-from-source and multi-step to set up). - Console login (GitHub OAuth) + key minting.
Proposed (ADR-035 — awaiting ratification)
- Signed, one-line distribution (
brew/curl | sh/winget/npx), replacing build-from-source. nightjar tunnel 5173— one command for local reach, defaultsplit./.well-known/nightjarclient discovery (kills the control-URL guessing).- Typed
E_LOCAL_REACH_NOT_READY+ alocal_reach_ensureMCP tool, so an agent hits a clear, claimable hand-off instead of a silent502. - A
reach:selfscope family so a driving identity can provision its own ephemeral reach without org-network admin power.
See also
- ADR-035 — Local Reach Grants — the distribution + first-mile-local-reach architecture this guide is the user-facing view of.
- local-server-via-nightjar-daemon.md — the full, tested Part 2 recipe as it works today.
- credentials-and-identity / credential-security-model — tokens, scopes, and the identity model behind the Bearer you paste. (porting to the docs site shortly.)