jun setup walks every step on this page interactively. This document is what the wizard does (skim it if a step’s failing) and what to do if you’re provisioning by hand.
Quick reference:
Terminal window
npmi-gjune
junsetup# this walkthrough, automated
juninstall-service# optional background unit
jundoctor# re-validate any time, non-interactive
Private install
While this repo is private, npm i -g june will not resolve. Two install paths work:
A. Direct from GitHub (recommended for your other machines).
Terminal window
# Either: gh auth login (uses your GitHub OAuth token)
# Or: add an SSH key to GitHub and set the npm protocol below.
npmi-ggithub:antonbalitsky/june-linear#v0.1.1
npm i -g github:<owner>/<repo>#<tag> clones the repo and runs npm install against it. It needs Git auth to GitHub — pick one:
HTTPS / gh token (simplest): run gh auth login once on the install machine. gh writes credentials that Git picks up via git credential-helper.
SSH: add the machine’s public key at https://github.com/settings/keys, then npm config set github:registry git+ssh://git@github.com: (or just rely on Git’s URL rewriting if you already use SSH for GitHub).
B. Tarball from the GitHub Release (no GitHub auth required).
Useful on an isolated VM, or anywhere you don’t want to register a GitHub token.
Terminal window
# From https://github.com/antonbalitsky/june-linear/releases — grab the .tgz.
npmi-g./june-0.1.1.tgz
The Release page lists every published version; download whichever tag you want to install. The tarball is exactly what npm pack produces, so npm i -g ./<file>.tgz is byte-identical to the eventual public npm i -g june.
When the repo goes public, both private-install paths above keep working unchanged. The README quickstart’s npm i -g june becomes the canonical command once the package is published to the public npm registry.
Prereqs (one-liner)
An always-on host — Linux VPS, dedicated Mac mini, or a laptop you’re going to leave open and plugged in. Webhook orchestrators are async-push, so a sleeping host degrades the experience. See deployment.md for the recommended setups + the macOS pmset recipe.
Node 22.11+ (june uses Node’s built-in node:sqlite module; no native deps. 22.11 is the first 22-series LTS — earlier 22.x releases had unstable SQLite APIs).
At least one runner CLI installed and logged in — Claude Code is the default. See step 6 for the others.
Linear personal API key.
A public webhook URL — the wizard helps with cloudflared / ngrok / tailscale, or you provide your own.
1. Linear personal API key
Use a separate Linear teammate for the agent. Don’t use your own personal API key. Invite a dedicated user (june-bot@yourdomain or similar) to your workspace and add them only to the teams the agent should operate on. Every comment the agent posts, every state move, every issue it creates will be attributed to that user — so it’s obvious in the activity feed what the agent did vs. what you did, and you can scope it down to a single team without losing access yourself. Bot loop protection (see concepts.md) keys on the bot’s email; with a dedicated user, that filter is unambiguous.
A personal API key impersonates the user it was created by, against the Linear GraphQL API. june uses it to read issue context, post comments, move workflow states, register webhooks, and (optionally, via linear-action) create new issues.
In Linear (signed in as the dedicated agent user): top-left avatar → Settings → API → Personal API keys → Create key. Direct URL: https://linear.app/settings/api.
Name it june-orchestrator-<host> and copy the lin_api_... value. Linear shows the key once — copy it before closing the dialog.
Paste into jun setup when prompted, or write to ~/.config/june/.env directly:
LINEAR_API_KEY=lin_api_...
The wizard validates the key against the viewer query and prints the resolved user + organization back to you. Confirm the email matches the dedicated user, not your own. If you skipped the wizard:
Terminal window
curl-sShttps://api.linear.app/graphql\
-H"Authorization: $LINEAR_API_KEY"\
-H'Content-Type: application/json'\
--data'{"query":"{ viewer { email } organization { name } }"}'
Keys can be revoked from the same Settings page. Rotate freely — jun doctor will tell you immediately if the key starts failing.
Adding the agent user to Linear
If you don’t already have a dedicated user, create one before jun setup:
In Linear: Settings → Workspace → Members → Invite member.
Use a separate email you control (Gmail’s + aliasing works: anton+june@…). Linear will send an invite there; accept it.
Add the new user only to the teams the agent should touch. Skip teams that hold work the agent shouldn’t read or post in.
Sign in to Linear as that user, generate the API key per the steps above, sign back out.
The agent’s email becomes the value JUNE_BOT_USER_EMAILS filters on. jun setup auto-resolves it via the viewer query and adds it; you only need to set JUNE_BOT_USER_EMAILS explicitly if you have multiple bot accounts in the same workspace.
2. Team selection
A single Linear webhook is scoped to one team. The wizard lists every team your key can see and lets you pick by number or by team key (ENG, JUN, etc.). The choice is stored as JUNE_TEAM_KEY in .env.
JUNE_TEAM_KEY is required for the setup scripts (ensure-states, ensure-labels, register-webhook, acceptance:live) and for the waiting sweep. At runtime the orchestrator reads the team key from each incoming webhook, so a wrong JUNE_TEAM_KEY only breaks the scripts that explicitly target a team — not serving.
3. Workflow states and labels
june expects two workflow states in the target team:
State
Type
What june does
Ready for Review
completed
Set when a run succeeds (verified result, no blockers).
Blocked
canceled
Set when a run fails verification or is manually stopped.
If your team already has equivalent states under different names, set JUNE_REVIEW_STATES / JUNE_BLOCKED_STATES instead of creating new ones — see configuration.md → States.
And five labels (any unlabeled issue still routes to Claude — see runners.md):
Label
Purpose
codex
Route this issue to Codex CLI.
opencode
Route this issue to OpenCode.
gemini
Route this issue to Antigravity CLI (agy).
hermes
Route this issue to Hermes Agent.
goal
Wrap the first prompt with /goal (Claude / Codex / Antigravity only).
jun setup creates the missing states and labels for you (idempotent — it’s safe to re-run). To do it by hand:
Terminal window
npmrunensure-states# creates Ready for Review + Blocked if missing
3. tailscale funnel — free with a tailnet; URL is *.ts.net (stable, not ephemeral)
4. self-hosted — reverse proxy / public VPS / etc.
The wizard auto-detects which binaries are on your $PATH and picks the first one found as default. If the binary’s not installed, it prints the install command and the link to the vendor’s install page.
Option A: Cloudflare Tunnel (recommended for local dev)
Free, no account required for ephemeral tunnels, no firewall rules to open.
Terminal window
brewinstallcloudflared# or apt/dnf — see vendor link
cloudflaredtunnel--urlhttp://localhost:8787
Watch the output for the https://<random>.trycloudflare.com URL it prints. Paste that into the wizard. The tunnel keeps running in that terminal; when you stop it the URL is gone — fine for short sessions, not for unattended deployments. For a stable URL, run a named tunnel.
Option B: ngrok
Terminal window
brewinstallngrok
ngrokconfigadd-authtoken<your-token>
ngrokhttp8787
Free plan rotates the URL on every restart, which means you have to re-register the Linear webhook every time (jun setup again will do it). Paid plan gives you a stable subdomain.
Option C: Tailscale Funnel
If you already run Tailscale, Funnel exposes a single port on a stable <machine>.<tailnet>.ts.net hostname over HTTPS:
If you’re putting june on a server with a real domain, terminate TLS at your reverse proxy (Caddy, nginx, Traefik) and forward to 127.0.0.1:8787. Bind june to loopback (JUNE_HOST=127.0.0.1, the default) so it isn’t directly reachable.
A minimal Caddy block:
june.example.com {
reverse_proxy 127.0.0.1:8787
}
What the wizard writes
A 32-byte random LINEAR_WEBHOOK_SECRET is generated. The webhook itself is registered against Linear with the team scope you picked. If a june-labelled webhook already exists for that team, the wizard offers to update its URL.
Setting up by hand instead? Generate the secret with openssl rand -hex 32, put it in .env, then:
Setting JUNE_ADMIN_TOKEN gates the /admin/* endpoints behind Authorization: Bearer <token>:
POST /admin/reconcile — mark stale running DB rows orphaned, comment, and move issues to Blocked. Used when the orchestrator restarted while a run was in flight.
POST /admin/runs/:id/stop — SIGTERM a specific run’s process group, then SIGKILL after 5s.
If unset, both endpoints respond 403 admin disabled. The wizard offers to generate a 24-byte hex token.
6. Installing runner CLIs
june spawns one of these as a subprocess per Linear issue. You need at least one installed and authenticated. Claude is the default and the one runner whose absence prevents boot — the rest are opt-in by label and fall back to Claude with a comment if missing.
hermes init, then configure a provider/model; lives in ~/.hermes/
jun setup and jun doctor both print which CLIs were found on $PATH. Set JUNE_CLAUDE_BIN, JUNE_CODEX_BIN, JUNE_OPENCODE_BIN, JUNE_ANTIGRAVITY_BIN, or JUNE_HERMES_BIN if your install lives somewhere unusual — especially under launchd / systemd, where PATH is stripped.
7. First-run verification
Terminal window
jundoctor# green/red checklist
junstart# foreground; Ctrl-C to stop
Then in Linear, create a tiny issue in the team you registered the webhook for, in any non-Backlog state. Within a few seconds:
the issue moves to In Progress (or whatever JUNE_WORKING_STATES resolves to);
a comment appears (either a progress comment from the agent, or the final reply when the agent exits);
the issue moves to Ready for Review on success or Blocked on failure.
If anything goes wrong: jun logs -f to follow, jun doctor to re-check probes, jun config show to see the resolved values. The troubleshooting page lists every common failure mode fix-command-first.
jun install-service fills templates/launchd/june.plist (macOS) or templates/systemd/june.service (Linux) with your absolute paths and current user at install time — no hard-coded username. The unit reads secrets from your ~/.config/june/.env, not from the unit file. See the templates for the exact shape if you’d rather hand-edit.
Docker is the third option for long-running deployments:
Terminal window
dockercomposeup
The image ships the orchestrator only — runner CLIs have their own auth and must either be installed in an extended image or mounted in from the host. See Dockerfile comments for both patterns. jun doctor from inside the container reports which runners it can see.
What ~/.config/june/.env looks like
The wizard writes it with mode 600 and backs up the previous version. The minimum surface:
JUNE_AGENT_ROOT=/path/to/workspace # optional; defaults to cwd
jun config show prints the resolved values (secrets masked) and which file they came from. Everything else has a default — full list in configuration.md.