Skip to content

Setup

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
npm i -g june
jun setup # this walkthrough, automated
jun install-service # optional background unit
jun doctor # 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.
npm i -g github: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.
npm i -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.

  1. In Linear (signed in as the dedicated agent user): top-left avatar → SettingsAPIPersonal API keysCreate key. Direct URL: https://linear.app/settings/api.
  2. Name it june-orchestrator-<host> and copy the lin_api_... value. Linear shows the key once — copy it before closing the dialog.
  3. Paste into jun setup when prompted, or write to ~/.config/june/.env directly:
    LINEAR_API_KEY=lin_api_...
  4. 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 -sS https://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:

  1. In Linear: SettingsWorkspaceMembersInvite member.
  2. Use a separate email you control (Gmail’s + aliasing works: anton+june@…). Linear will send an invite there; accept it.
  3. 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.
  4. 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:

StateTypeWhat june does
Ready for ReviewcompletedSet when a run succeeds (verified result, no blockers).
BlockedcanceledSet 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):

LabelPurpose
codexRoute this issue to Codex CLI.
opencodeRoute this issue to OpenCode.
geminiRoute this issue to Antigravity CLI (agy).
hermesRoute this issue to Hermes Agent.
goalWrap 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
npm run ensure-states # creates Ready for Review + Blocked if missing
npm run ensure-labels # creates codex / opencode / gemini / hermes / goal

4. Webhook ingress + tunnel choice

Linear needs to reach your orchestrator over the public internet. jun setup presents four options:

1. cloudflared — free, fast, ephemeral *.trycloudflare.com URL
2. ngrok — free tier works; requires `ngrok config add-authtoken …` once
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.

Free, no account required for ephemeral tunnels, no firewall rules to open.

Terminal window
brew install cloudflared # or apt/dnf — see vendor link
cloudflared tunnel --url http://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
brew install ngrok
ngrok config add-authtoken <your-token>
ngrok http 8787

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:

Terminal window
tailscale funnel --bg --https=443 http://localhost:8787

Docs: https://tailscale.com/kb/1223/funnel. Restricted to ports 443 / 8443 / 10000; rate-limited but free.

Option D: Your own reverse proxy / hosted box

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:

Terminal window
JUNE_PUBLIC_WEBHOOK_URL=https://<your-url> npm run register-webhook

5. Admin token (optional)

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.

RunnerInstallAuth
Claude Codehttps://docs.claude.com/en/docs/claude-code/installclaude once to log in to your Anthropic account
Codex CLIhttps://github.com/openai/codexcodex login (OpenAI account) once on the host
OpenCodehttps://opencode.aiopencode auth login once on the host
AntigravityInstall the Antigravity desktop app from https://antigravity.google; agy ships with itSign into the desktop app; agy reuses the desktop session token
Hermes Agenthttps://github.com/lukestanley/hermeshermes 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
jun doctor # green/red checklist
jun start # 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.

8. Running as a service

Terminal window
jun install-service # macOS: launchd. Linux: systemd --user.
jun status # is it running?
jun logs -f # follow
jun uninstall-service # the inverse

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
docker compose up

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:

LINEAR_API_KEY=lin_api_...
LINEAR_WEBHOOK_SECRET=...
JUNE_TEAM_KEY=ENG
JUNE_PUBLIC_WEBHOOK_URL=https://example.trycloudflare.com/webhook/linear
JUNE_ADMIN_TOKEN=... # optional
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.