@mainwave/cli
v0.10.0
Published
Setup wizard for connecting third-party coding tools to Mainwave AI.
Readme
@mainwave/cli
Setup wizard that connects third-party coding tools to Mainwave AI, so their OpenTelemetry traces land in your dashboard, attributed to you.
Installs as the mainwave-cli command. Requires Node 22.13 or newer.
Supported tools
| Adapter | Tool |
| ------------------- | ------------------------------- |
| claude-code | Claude Code |
| claude-desktop | Claude Desktop (Cowork) |
| codex | Codex CLI |
| copilot-cli | GitHub Copilot CLI |
| copilot-chat | GitHub Copilot Chat in VS Code |
| cursor | Cursor |
| cursor-enterprise | Cursor (Enterprise OTel export) |
| kiro-ide | Kiro IDE (1.0 or newer) |
| kiro-cli | Kiro CLI (2.13 or newer, V3) |
Setup
npx @mainwave/cli setupWith no adapter named, the wizard lists the tools it can configure. Anything detected on this machine is checked and can't be unchecked — it's installed here, so leaving it out just means it goes unmonitored — and everything else starts unchecked, so a tool you don't use gets no config file. Whatever you end up with is set up in one pass: the questions are asked once, every file is previewed together, and one confirmation writes them all. Name adapters explicitly to skip the picker:
npx @mainwave/cli setup claude-code codexSigned in (mainwave-cli login), setup asks almost nothing: it mints a Mainwave key labelled with
your email and this machine so you never handle a credential, takes the collector URL from your
deployment, writes each tool's config in the format it expects, and then sends a sample trace and
watches for it to land. Not signed in, it prompts for a key; that path stays for scripts and
air-gapped installs.
There is no use case to pick. For every tool but one (below), setup writes no use-case id: Mainwave
attributes each tool's activity to you from the identity its telemetry already carries — your Claude
account for Claude Code and Claude Desktop, your email for the rest — and files it under your use
case for that tool. Your activity
appears under Development in the app once your first session arrives, within about 10 minutes.
Codex and Copilot carry no identity of their own, and Claude Code carries none when it signs in with
an API key, Bedrock or Vertex rather than a Claude account, so setup adds your email for them: the
signed-in account's, or --email, or your git email. Under a Claude account the account still wins,
so the email changes nothing there. If setup has no email, it warns that those tools' activity will
land unattributed until you re-run with --email or sign in.
The exception is cursor-enterprise. Cursor's Enterprise OTel export is configured once for the whole
team, by an admin, and carries no per-person email, so an email would attribute the whole team to
whoever ran setup. Setup gives it a team use-case id instead: minted on the first run, kept on every
re-run, and never shared with your other tools. If the id it already has is also in your other
tools' config, or is your own stored use case — which is what earlier versions of the CLI wrote —
setup mints a separate team id and says so.
Kiro has no exporter of its own to point at Mainwave, so setup installs a Kiro hook instead: one
file, ~/.kiro/hooks/mainwave.json (other files in that directory are left alone), that runs a small
script on session start, prompt submit, each tool call, agent file changes, the end of each turn and,
in Kiro CLI, session end. Each event becomes one span carrying the Kiro session id, the event, the
tool name and the kind of file change — never file paths, tool input or output. Prompt text is off
for Kiro unless you pass --opt-in prompts. The script prints nothing, always exits successfully and
hands the upload to a background process, so it never blocks, slows or steers a Kiro session. Your
email is the account kiro-cli whoami reports — it is how Kiro activity is matched to Kiro's own
usage export — unless you pass --email; without a Kiro sign-in it falls back to your Mainwave
sign-in, then your git email. Kiro CLI records sessions only in V3 mode (kiro-cli --v3); its 2.x
default mode doesn't read global hooks. Kiro hooks carry no tokens or cost, and --ucid doesn't
apply to them: Kiro activity is always attributed from your email. To take it off again:
mainwave-cli setup kiro-ide --remove # or kiro-cli; both together to remove everythingKiro IDE and Kiro CLI run the same hooks file, and the hook can't always tell which one ran it. With only one of them set up, sessions from the other are reported under the one you set up. With both set up, Kiro CLI run in an editor's terminal (VS Code, Cursor, Kiro) is reported as the IDE, apart from its session-end event. After you remove kiro-ide, that terminal session is taken for the IDE and dropped, apart from its session-end event; after you remove kiro-cli, it is still reported as the IDE, and Kiro CLI in a plain terminal is dropped.
Removing one of the two keeps the hooks for the other, and the hook stops sending the removed one's
sessions; removing the last one removes Mainwave's entries from ~/.kiro/hooks/mainwave.json (the
file too, if nothing else is in it), Mainwave's instructions from ~/.kiro/steering/mainwave.md
(likewise) and ~/.mainwave/kiro.env with its backups (they hold your key; setup writes it again).
Validation names the organization the key belongs to, so a key from the wrong workspace is caught
before anything is written. To pin every tool to one specific use case instead — a shared or team
one — pass --ucid <id|url> or --use-case <name>. A pin you set that way stays: re-running setup
without either keeps it in the tools that already carry it, and says so. --no-use-case removes it.
A re-run does remove a use-case id an earlier version of the CLI assigned on its own, and tells you
which.
Config is written to your user-global settings file — ~/.claude/settings.json and friends
(for Claude Code, $CLAUDE_CONFIG_DIR/settings.json when you set CLAUDE_CONFIG_DIR).
There is no repo-local option: it put your API key inside the repository, several tools ignore
credentials that come from repo config, and it had to be redone for every checkout.
Re-run it any time. Existing values pre-fill the prompts, and opting back out of a setting removes it rather than leaving it stale.
What gets captured
Setup enables content capture by default: prompt text, shell commands and file paths,
tool result bodies and, for Claude Code, repository names (OTEL_METRICS_INCLUDE_REPOSITORY,
which stamps the session's repository on Claude Code's own events, metrics and trace spans) are
sent to your collector alongside the metadata (tool names, tokens, cost, who ran what). The wizard
says so before it finishes — it is not a question, because what an organization collects is a
policy decision rather than a per-developer one.
To send metadata only:
npx @mainwave/cli setup claude-code --opt-in noneFor Claude Code that still leaves every activity view working — tool names, tokens, cost, sessions
and per-person activity are all metadata. Codex is different: it reports tool names, success and
approvals only when tool logging is on, so --opt-in none leaves Codex's tool views empty. Keep
--opt-in tool-details for Codex if you want those without sending prompt text or tool output.
Or pick exactly what you want: --opt-in tool-details, --opt-in prompts,tool-details,
--opt-in tool-details,repository, and so on.
Re-running with a different --opt-in removes the flags it no longer wants rather than leaving them
stale.
Restart your coding sessions afterwards. These tools read their telemetry config when they
start, so a session that was already open keeps the old settings and sends nothing — including the
one you ran setup from. verify can't catch that: it proves the key and the network path, not that
a tool picked the file up.
Proving it works
mainwave-cli verify # push a sample trace, wait for it to land, print the link
mainwave-cli verify --json
mainwave-cli verify --no-sample # with a pinned use case: just watch for its spans, send nothingWith a use case pinned — --ucid here, or one a setup --ucid / --use-case run pinned that one
of your tools' config still carries — the sample trace carries that id, verify watches for it,
and the link goes to that use case. Otherwise the sample carries no use-case id at all — the CLI
finds it again by the trace id it generated, so checking your setup creates no use-case id in
Mainwave — and the link points at Development, where your tools' own sessions appear. --no-sample
needs a pinned use case; with none there is nothing to watch, and it says so. setup runs verify
for you at the end when you're signed in, watching only what that run pinned: a run without
--ucid or --use-case always sends the id-less sample. On a timeout verify lists what usually
causes it — the tool needs restarting, a DISABLE_TELEMETRY=1 in your shell, a collector URL that
doesn't match — and exits non-zero, so CI can gate on it.
Scripted and agent-driven runs
Every prompt has a flag twin, and --json makes stdout a single document (the narrative goes to
stderr), so a coding agent can drive the CLI without a terminal:
mainwave-cli setup claude-code codex \
--opt-in none --no-markers --yes --json| Flag | Answers |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| --api-key <k> | the collector key (otherwise minted for you when signed in) |
| --collector-url <u> / --api-url <u> | which deployment |
| --ucid <id\|url> / --use-case <name> / --no-use-case | pin one use case, or --no-use-case to unpin (default: none) |
| --email <e> | attribution identity for tools that need it injected |
| --opt-in prompts,tool-details,tool-content,repository (or all / none) | content capture (default: all on; none sends metadata only; repository is Claude Code's repository names) |
| --no-markers | skip the Mainwave hooks (installed by default): the ticket & PR markers and the Claude plan snapshot |
| --new-key | mint a fresh machine key instead of reusing this machine's |
| --yes | write without the confirmation prompt |
| --no-verify / --verify-timeout <s> | the closing first-span check |
| --skills / --no-skills | the agent skills payload |
Without a terminal the CLI never prompts: it answers from flags and environment, or fails naming the
flag it needed. status and doctor also take --json.
Agent skills
Setup installs three skills alongside the telemetry config — one detection, one pass, two outputs:
mainwave-setup— lets your coding agent run this whole flow itself: sign in, configure a tool, verify. It knows the flags, so you can just ask it to connect you.mainwave-instrument— wires themainwavePython SDK into an application, so an agent or service of your own reports runs, tokens, and cost.mainwave-instrument-node— wires the@mainwave/sdkNode.js SDK into a Node or TypeScript application (eve, the Vercel AI SDK, OpenAI, Anthropic), so it reports runs, tokens, and cost. Your agent picks it for a Node repository andmainwave-instrumentfor a Python one.
The bundles are copied to ~/.mainwave/skills/, then registered on each tool's own instruction
surface: a native skill directory for Claude Code, AGENTS.md for Codex, copilot-instructions.md
for Copilot, a rules file for Cursor, a steering file for Kiro. Claude Desktop has no such surface and is reported as a no-op
rather than counted as installed.
The payload is installed at user scope, like the telemetry config — the skills teach an agent how to use Mainwave anywhere, so a copy per repo would be pure duplication. Re-running setup refreshes them, the same way it refreshes the hook scripts.
mainwave-cli setup claude-code --skills-only # instructions, no telemetry config
mainwave-cli setup claude-code --no-skills # telemetry config, no instructionsInside a coding agent, sign in with --device: an agent's shell has no terminal, so without it the
CLI assumes nobody is watching and falls back to email and password instead of opening the browser.
mainwave-cli login --deviceTicket & PR markers
For Claude Code, setup also installs ticket & PR markers: small post-tool hooks that emit one
event when a Linear or Jira issue is filed and one when a pull request is opened with
gh pr create or a GitLab merge request is opened with glab mr create (or a glab api POST to a
project's merge_requests endpoint). Claude Code starts the pull-request hooks only for a Bash
command that begins with gh or glab (a wrapper or a path, such as env gh … or
/usr/local/bin/glab …, isn't matched), and runs every Mainwave hook in the background, so none of
them adds time to a tool call. They are what populates "Tickets filed" and "PRs created" on the use case — without
them those counts read zero, which is why they are on by default rather than a question.
What leaves your machine: the issue key, whether it was a create or an update, and the PR's url/number/title plus aggregate diff counts. For a GitLab merge request that is the url, number, title, project id and the changed-file count. Never ticket bodies, never diff content. The hooks fail open — a telemetry problem can never block or slow a coding session — and count forward only from when you enable them.
For Claude Code, the same install adds a Claude plan snapshot: a hook on Claude Code's
UserPromptSubmit event (each prompt you submit) that reads Claude Code's local account record (.claude.json) and sends one event every time
it runs. What leaves your machine: your Claude account id, organization id and email (the identity
Claude Code's own telemetry already carries); from your account profile, exactly as Claude Code
stores them, the plan's organization type, seat tier, rate-limit tier and billing type, your
organization and workspace roles, when the subscription started, the trial's end date and length,
the date of your first Claude Code use, and when Claude Code fetched that profile; from Claude
Code's usage cache, your 5-hour and 7-day usage and when they reset, each rate limit's percent used,
group, severity, whether it is the active one and when it resets (and, for a limit Claude Code
applies per model, which model it is, by the name Claude Code stores), whether extra usage is on, the
extra-usage amount spent and monthly limit, their currency, whether the spend limit is reached,
whether and why extra usage is disabled, and when Claude Code fetched those figures. Each event also
carries which event sent it, the prompt id, the
session id, the ids of the turn's trace and span, and the session's OpenTelemetry resource attributes, as the
other markers do. Never your prompt, and nothing else in that file. A figure Claude Code hasn't
recorded is left out, not sent as zero; Claude Code refreshes its usage figures only when you open
its usage view, so the same figures repeat until then, each with the time Claude Code fetched it. Figures
refreshed by a /usage just before you quit reach Mainwave at your next prompt, since Claude Code
keeps them on disk. Nothing is sent when a session ends.
It runs only in sessions signed in with a Claude account, keeps no state it reads back (a one-line
timestamp file, ~/.mainwave/logs/plan-snapshot.log.ok, records the last successful send for doctor),
and never makes a
prompt wait: Claude Code runs it in the background without waiting for it, and it gives up on the
send after 4 seconds. Under claude -p, Claude Code stops a snapshot that is still sending when the
run ends, so that snapshot is lost. Setup says that it is on, and what it sends, on every run.
It has no switch of its own: it installs wherever setup installs the Mainwave hooks for Claude Code,
and --no-markers, which installs no Mainwave hooks at all, leaves it out too.
Where Claude Code runs only the hooks your organization's managed settings deliver
(allowManagedHooksOnly), or a settings file turns hooks off (disableAllHooks), and the managed
settings don't send the snapshot themselves, your own copy can't run; setup says so and names the
setting.
It comes with one smaller Claude account event. When a turn ends on an API error (a rate limit, a billing error, an overloaded API, a sign-in that stopped working), one event carries the error type only — never the error details or message. It also carries the session id, the prompt id, the turn's trace ids and, when the session is signed in with a Claude account, your Claude account id and email, so it lines up with the plan snapshot. Unlike the snapshot, it is sent from any Claude Code session.
Mechanics: every Mainwave hook group in your settings runs one small shell entry,
~/.mainwave/hooks/managed/mainwave-hook.sh, with the event it is for (claude-code PostToolUse gh,
claude-code UserPromptSubmit, …). The entry prints nothing, never reads the hook input itself
(only Codex's one Bash hook does, to skip commands that can't open a PR or MR), and exits 0 whatever
happens; it runs the current release's scripts, kept in ~/.mainwave/hooks/managed/versions/.
Setup also adds a session-start hook that runs
{ cd "$HOME" && npx -y --registry=https://registry.npmjs.org @mainwave/cli@latest install-hooks --quiet; } >/dev/null 2>&1; exit 0
in the background when a session starts, resumes or forks — its output discarded and exit 0
whatever happens, so an unreachable npm registry never shows up as a failed hook: it installs the latest release's scripts
and points the entry at them, and keeps the hooks setup installed in your user settings current —
on the events setup told you about only. A release that adds a new kind of hook waits for your next
setup run, which says what it sends. It is written only when at least one hook is on, so
--no-markers writes no hooks at all. Your --no-markers choice is also recorded as a file
under ~/.mainwave/hooks/off/, which the entry reads, so a hook you turned off sends nothing even
from a copy setup didn't write (unless your organization's managed settings deliver it, which you
can't turn off). Re-running setup removes hooks written by an earlier CLI version, and the scripts
an earlier version copied into ~/.mainwave/hooks/ once nothing runs them. To disable the hooks,
the plan snapshot included, re-run setup with --no-markers — the entries are removed cleanly. doctor checks that the entry is installed, that it names an installed release with every
script, and that every Mainwave hook group is in the background and narrowed by its matcher.
On Windows, the Claude Code hooks need Git for Windows. Claude Code runs hook commands through Git
Bash, and through PowerShell when it finds none, where the entry's shell command can't run. Without
Git Bash, setup installs no Claude Code hooks and says why, and doctor reports it as an error;
hooks an earlier CLI version installed that run node directly are kept. Claude Code looks for Git
Bash at CLAUDE_CODE_GIT_BASH_PATH, then Git for Windows' default install paths, then beside the
git on your PATH, and setup looks in the same places: if yours is elsewhere, set
CLAUDE_CODE_GIT_BASH_PATH to its bash.exe and re-run setup.
Organizations that configure Claude Code centrally (managed settings from the Claude admin console,
or an MDM managed-settings.json) deliver the markers and the Claude plan snapshot without anyone
running setup: when a session starts, resumes or forks (not on /clear or a compaction), the
managed settings run the same install-hooks command — from your home directory, so a repository's
own .npmrc or node_modules can't choose what runs — which installs or refreshes the scripts in
~/.mainwave/hooks/managed/ with no prompts, no sign-in and no network calls of its own. @latest
means each new release reaches your machine at its next session start, whatever copy of the CLI you
have installed: npx asks the registry for the current release each time, and while you're offline
the scripts already installed keep working. Each release's scripts sit in their own directory and
the entry is switched to a new one only once it is complete, so two installs racing never mix
releases. The org-wide payload writes exactly the hook commands setup writes, so on a machine with
both, Claude Code runs each hook once. Managed settings your admin pasted before the entry existed
run one script per hook, in the foreground, and have no plan snapshot; they keep working, and get
the current hooks only when your admin pastes the current version from Mainwave's Org-wide setup,
since install-hooks refreshes the scripts, not your organization's managed settings. An admin who
adds the current version without removing the earlier one gets every marker twice; doctor flags
both being present. When managed settings deliver the
hooks, install-hooks also removes your user settings' copy of each hook they deliver, so each ticket and PR is marked once and each prompt sends one plan snapshot, and setup itself leaves
out any hook your managed settings already run. Your own session-start hook stays: while the
managed settings run the same command Claude Code runs it once (a payload pasted before the
silent form runs beside it until your admin re-pastes — harmless), and if they stop delivering the
hooks it puts back the ones setup installed for you. It never edits project settings; doctor lists
every settings file that installs the hooks and warns when two of them would fire for the same
ticket or PR. The org-wide setup follows how Claude Code 2.1.282 and later apply managed settings;
doctor warns on an older install.
Two kinds of session don't get the admin console's markers or plan snapshot:
- A session that skips server-managed settings: one started with a
CLAUDE_CODE_USE_*provider variable or a non-AnthropicANTHROPIC_BASE_URLin your shell or user settings. Once the org's managed settings deliver the markers,install-hookshas removed the per-developer copy, so such a session sends no markers;doctorrun the same way says so. MDM ormanaged-settings.jsondelivery doesn't have this gap. - A non-interactive run (
claude -p, the Agent SDK, CI) on a machine where nobody has approved the org's current settings in an interactive session. Claude Code applies them for that run only and doesn't cache them, so the marker hooks can't read the org's collector endpoint and key: they fall back to your user settings or send nothing, and on a machine that also has a per-developer setup both copies fire, marking each ticket and PR twice. Approve the settings in an interactive session on that machine (again after each change to them), or deliver them through MDM ormanaged-settings.json, which need no approval.
Under the org's settings, which carry no use case of their own, a marker is attributed through the
Claude account the session is signed in with. A session signed in another way (an API key, a
CLAUDE_CODE_OAUTH_TOKEN, a cloud provider through MDM delivery) sends markers no developer is
attached to, and no plan snapshot.
Markers are available only for tools with a hooks mechanism (today: Claude Code and Codex; the plan snapshot is Claude Code only). For other tools the wizard says so instead of appearing to succeed.
Signing in
mainwave-cli login # shows a code and opens Mainwave to approve it
mainwave-cli whoami # who you are, which org, and where telemetry goes
mainwave-cli logout # forget the session (configured tools keep working)login shows a short code, opens the approval page in your browser, and waits. You approve inside
the Mainwave session you already have — no password is typed into a terminal — and the page shows
which machine is asking so an unexpected request is obvious. Approve or deny; denying stops the
terminal immediately.
mainwave-cli login --no-browser # print the URL instead of opening it (SSH, remote boxes)
mainwave-cli login --device # browser flow with no terminal (coding agents)
mainwave-cli login --password # email + password instead of the browserHaving no terminal usually means nobody is watching, so the CLI falls back to the password path —
which is right for CI and wrong for a coding agent, where a human is sitting right there able to
approve. --device says so explicitly and forces the browser flow. --password and an explicit
--email / --password value still win over it.
Scripts and CI sign in without a terminal by setting MAINWAVE_EMAIL and MAINWAVE_PASSWORD,
which also selects the password path (the --password flag picks the flow; it never takes the
password itself — argv lands in shell history and in ps). Point the CLI at a non-production
deployment with --api-url, or MAINWAVE_API_URL / MAINWAVE_APP_URL /
MAINWAVE_COLLECTOR_URL.
~/.mainwave/settings.json holds the non-secret intent (org, endpoints, a pinned use case) and
~/.mainwave/credentials.json holds the session token — both mode 0600, and the token is never
written into any tool's settings file.
Other commands
mainwave-cli status [<adapter>] # on-disk config for every tool, or just one
mainwave-cli doctor <adapter> # settings hierarchy + health checks
mainwave-cli doctor <adapter> --validate # also auth-check against the collector
mainwave-cli edit <adapter> # open the settings file in $EDITOR
mainwave-cli env <adapter> # print shell export lines
mainwave-cli install-hooks [--quiet] # install or refresh the Mainwave hook entry and the latest release's scripts
mainwave-cli --helpdoctor walks the tool's full settings hierarchy (managed → project-local → project → user), prints
the effective configuration tagged by which file each value came from, and flags the gotchas that
silently drop telemetry — a kill-switch set in your shell environment, a missing endpoint, or a
managed-tier policy overriding what you just wrote. For Claude Code the managed tier includes the
settings your organization's admin console delivers, read from Claude Code's own cache of them. A tool
with no use-case id is the normal state, not a problem: doctor notes that attribution happens at
ingest. It also fails (exits non-zero) when a tool's activity would land unattributed: a tool that
needs your email has none — Codex or Copilot, or Claude Code when it isn't signed in to a Claude
account (an API key, Bedrock or Vertex, or no sign-in at all) — or cursor-enterprise has no team
use-case id.
From Claude Code 2.1.282, project and local settings (.claude/settings.json,
.claude/settings.local.json) can no longer turn telemetry on, choose where it goes, or choose what
it captures: Claude Code ignores those keys there. doctor, status and edit --local say so, name
the file and the keys, and point you at your user or managed settings instead. OTEL_RESOURCE_ATTRIBUTES
still applies from project settings, so a per-repository use case keeps working. The ticket and PR
marker hooks follow the same rule: they take the collector endpoint and key only from managed
settings, your user settings or the process environment, never from a repository's settings.
Some tools read their configuration from the environment rather than from a file. For those, env
prints the export lines to source:
eval "$(mainwave-cli env copilot-cli)"Where your API key ends up
The file the wizard writes contains your API key in plain text, so it is created with 0600
permissions in your home directory — never inside a repository. status shows every file the CLI
knows about, masked; doctor <adapter> walks the full hierarchy, including repo-local files an
older CLI or a colleague may have written.
Removing Mainwave
Deleting ~/.mainwave is safe. Every Mainwave hook checks that its script is there and exits
quietly when it isn't, so nothing reaches your coding sessions. Where your organization delivers
Mainwave through Claude Code's managed settings, the next session start reinstalls what the hooks
run and telemetry resumes on its own. A setup you ran yourself stays off until you run
mainwave-cli setup again — signed in, that creates a new key, since the record of the old one
lived there — and Cursor and VS Code Copilot Chat stop sending until then.
To stop entirely, also remove the Mainwave entries from your tools' settings — for Claude Code,
mainwave-cli setup claude-code --no-markers removes its hooks, or delete the hook groups that
name mainwave-hook.sh, install-hooks or --managed-by @mainwave/cli from
~/.claude/settings.json, along with the OTEL_* telemetry keys setup wrote. Hooks delivered
through your organization's managed settings stay until your Claude admin removes them. Hooks
now log under ~/.mainwave/logs/; releases before that logged to ~/.mainwave-*.log files in your
home directory, which are left for you to delete.
License
Apache-2.0
