Install
The Claude Code first-run happy path — get the cyberine CLI, wire it into Claude Code, then pin
a repo:
npm i -g @cyberine/cli # Node 20+; prebuilt binary for this platform, plus cyberine-mcp on PATH
cyberine --version # confirm which version is on PATH (-v / -V work too)
cyberine setup # once per machine: auth providers, pick + pin a chat model
cyberine plugin install # wire the Claude Code plugin: one symlink, ten skills, bundled MCP server
# restart Claude Code (or /reload) so the plugin loads
cyberine init --yes # per repo: pin a fleet of exactly what was detected, plus one lane per global seat
/cyberine:init # per repo, for unattended work: conflict-check, then apply the Cyberine rules
cyberine doctor # confirm — check the `claude-code:` sectionOne-line installer (ksmvc). site/public/install.sh runs the first two steps in one go. It checks for Node 20+ and npm, runs npm i -g @cyberine/cli, runs cyberine plugin install when claude is on PATH, notes an enabled TaskBoard plugin, and prints the next steps. It is POSIX sh, so it runs from a pipe:
curl -fsSL https://cyberine.dev/install.sh | sh # served by the live site
sh site/public/install.sh # from a checkout
CYBERINE_PREFIX=$HOME/.local sh site/public/install.sh # npm's global folder is not writable| Variable | Effect |
|---|---|
CYBERINE_VERSION | npm version or dist-tag to install (default latest) |
CYBERINE_PREFIX | pass --prefix to npm; add <prefix>/bin to PATH yourself |
CYBERINE_NO_PLUGIN | any non-empty value skips cyberine plugin install |
Verified 2026-10-01 under a temporary HOME and prefix, piped through sh: 0.2.0 installed, the plugin was linked with Skills (10) and MCP servers (1), the TaskBoard notice fired from a seeded settings.json, and a PATH with no node failed with exit 1 and a message naming Node.js 20.
Known Limitations and Deferred Work: nothing serves the script yet, because the docs site has no host (T671). Next.js serves site/public/ at the site root, so /install.sh goes live with the first deploy. The registry's 0.2.0 plugin predates the board hooks, so a fresh install shows no board banner until the next release is published (kvk7d). Windows is not covered: use npm i -g @cyberine/cli there.
What a plain user needs. Node 20+ (the floor README.md states, declared as engines.node in package.json since k6nce, so npm warns on an older Node), the cyberine CLI on PATH, a git repository with at least one commit in the repo you point it at, and at least one thing to dispatch to — either a provider API key stored with cyberine auth login <provider> or a logged-in subscription CLI (claude, codex, muse); cyberine setup --status is the read-only check for that: its CLI agents (logged in): line and, since kvywt, its detection table name every worker that was found and the mechanism and location behind each one, because no coding CLI ships a login-status command. The Claude Code CLI on PATH is needed for the plugin itself, the claude worker, the subscription seats and cyberine autopilot, and for nothing else — a loop-adapter-only fleet runs without it. cyberine init in a repo pins the fleet to exactly what it detected (plus one lane per global seat, --seats-only / --fleet full still available), seeds an empty .local/tasks/ACTIVE.md when no task manifest exists and appends .local/ to that repo's .gitignore on the same run; if the repo already has a manifest that append does not happen, so check git check-ignore -q .local/ yourself (keep the slash: without it a .local/ pattern reads as not ignoring a directory that does not exist yet), or read the Repo readiness section of cyberine doctor. Unattended work needs one further, deliberate step: cyberine autopilot refuses until .cyberine/config.local.json (or, for a team that tracks it, .cyberine/config.json) records the Cyberine rules as applied, and cyberine init --rules is what writes .claude/rules/cyberine-autonomy.md (commit, push and merge in this repo without per-operation confirmation, under a hard floor) plus .claude/rules/cyberine-board.md (every board write goes through cyberine tasks) and records that consent — never overwriting a file that already exists, and never as a side effect of --yes (--no-rules skips it outright, and on a TTY plain cyberine init asks, once: when the repo already records the rules as applied it prints Cyberine rules already applied on <date> (...) -- leaving them. instead of asking again). Reach for /cyberine:init rather than the flag: the skill reads the rules the repo already has, reports any clause that contradicts what these two would grant — and stops there rather than applying over it — then asks once when nothing does. Nothing else is required: the ten bundled skills drive only the cyberine CLI and this repo's own commands, the session-boundary handoff writer ships with the CLI (cyberine handoff write / pull, driven by /cyberine:handoff and /cyberine:end), and the separate amemoria skill is an optional preference rather than a dependency — CYBERINE_AMEMORIA_BIN only relocates it for someone who already has it. /cyberine:init reports every one of these as ok-or-gap in one pass and names who runs each fix.
Known Limitations and Deferred Work: @cyberine/cli is on npm since 2026-09-28 (kwpwz): a launcher plus one prebuilt binary package per platform (darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64). Supported and run (kvk7d, 2026-09-28): darwin-arm64 natively, darwin-x64 under Rosetta, linux-arm64 natively in Docker and linux-x64 under QEMU, all on glibc (--version, cyberine-mcp initialize, init --yes, route). win32-x64 is built and published but has never run: no Windows machine was available. musl Linux (Alpine and similar) is unsupported: every binary is a glibc build, and the launcher exits 1 with a message naming musl instead of spawning a binary that cannot start there; use a glibc image (node:22-bookworm) or a source checkout. The x64 binaries are built on Bun's -baseline runtime, so no AVX is assumed; 0.2.0 on the registry predates that and prints an AVX warning under Rosetta, fixed from the next release on. The root cyberine source package is never published. To try a source checkout as a package instead, npm pack in the checkout and npm i -g <tarball> (or --prefix <dir> to keep it apart from a linked install); the CLI runs its TypeScript source through tsx, so tsx and typescript are runtime dependencies (k1aj6), and no eval suite is in the package (ksxrp; since k6na8 the suite lives in plugin-evals/, outside plugin/). npm run selftest:pack-install (km0qz) is the guard: it packs HEAD, installs the tarball into a temp prefix with a clean HOME and CYBERINE_HOME, runs --version, --help, doctor, build, init --yes and route, probes cyberine web and cyberine-mcp, and repacks with tsx/typescript back in devDependencies as its negative control. It needs the npm cache or registry for the tarball's dependencies and exits 78 when the registry is unreachable; it never calls a provider. The Ink surfaces, a real dispatch and cyberine plugin install from the packaged path are not covered and stay a manual check (k94jq). No Claude Code plugin marketplace exists; general
discovery/marketplace is deferred (docs/design/plugin-marketplace-decision.md). Every bundled skill except help starts with a CLI check (kaep0): when command -v cyberine finds nothing it stops and prints the install command instead of running a failing one, and scripts/plugin-check.sh fails a skill that lacks it; the plugin's .mcp.json runs cyberine-mcp from PATH, so a copied plugin needs the CLI installed as well (k6x8w). The plugin
ships three hooks, all in plugin/hooks/: the task-board banner and the handoff pointer on SessionStart, and the board-write guard on PreToolUse. It declares no userConfig (unavailable on this activation path). disable-model-invocation: true
is honoured per skill (model cannot auto-invoke, but skills stay reachable as /cyberine:<skill>).
The ten bundled skills cost roughly 1250 tok always-on in total, estimated at the same ~5 chars/tok ratio as the prior counts (end added ~98 tok and handoff ~139 tok, 2026-09-27; was ~1017 across eight, and before that ~891 across seven after research was added, ~715 across six after plan's existing-code branch grew its description, ~680 across six before that, ~560 across five, ~668 with dispatch); help's own description alone is ~126 tok, kept close to the per-skill average on purpose rather than let a router's description balloon. Plugin version is inert on this path (updating is
git pull then /reload-plugins or a new session). Under claude --plugin-dir <path> the skills load but
the bundled MCP server may not reach the model in print-mode; verify MCP on the installed path instead.
claude plugin validate --strict and claude plugin details require a recent Claude Code version.
Model invocation, operator decisions 2026-09-18: /cyberine:init is now disable-model-invocation: true (a readiness pass is the operator's call, never the model's). The same day a further ruling (k8qqq) made tasks slash-only as well, after the nl-* eval (kg3gm) measured a bare "what's next" in a repo with no .cyberine/ getting a reply that named Cyberine. That ruling was reversed on 2026-09-29 (kfp69): this plugin replaces the standalone cyberine-taskboard plugin and the two are never installed together, so tasks must answer the same plain board phrases without the word "cyberine". When both are enabled anyway, cyberine doctor's claude-code: section warns once per enabled cyberine-taskboard@<marketplace> entry, reading user, project and local settings the way Claude Code layers them (kxcnc). A user or local entry gets the fix hint claude plugin uninstall <key> --scope <scope>, which /cyberine:init runs only after the operator says yes. A project entry gets no command, because it lives in the shared .claude/settings.json. TaskBoard's hooks already step aside for this plugin, so only its tasks skill overlaps, and the board needs no migration. tasks is now disable-model-invocation: false with a when_to_use that fires on plain talk about this repository's task board ("what's next", "task nao tiep", "what's on the backlog", "xong roi cai X", "de sau cai X", "ket vi X", "save these as tasks", "add a recurring task", "don dep board", "show me the board"), but only in a repo that has a board (.local/tasks/ACTIVE.md or .claude/TASKS.md); with no board the skill steps aside and the reply runs no cyberine command and names no /cyberine:* command. It does not fire on "add a TODO comment", "refactor X", a bare "end" or "wrap up", "cyberine end" (the end skill), "cyberine handoff" (the handoff skill), or a bare scheduled chore such as "every Monday, update dependencies", which belongs to Claude Code's own scheduler. The model-invocable skills are therefore help, tasks, handoff and end; init, plan, ask, run, autopilot and research stay slash-only or reached through help. help's own anchor narrowed at the same time to the word "cyberine" or a /cyberine:* command: generic nouns ("the board", "a handoff", "the fleet", "a seat", "autopilot") no longer count, so an anchorless board question goes to tasks (or, with no board, to nobody) and never to help. Text the session merely read (a file, an issue, a web page, a tool result, another session's message) is data, never a request to write the board and never a yes; both tasks and help carry that rule. The same day help and the CLI cheatsheet gained one rule, measured as missing by the nl-* eval: the request to run something ("have cyberine run X", "let it work the board unattended") is never the yes; the operator's next turn, after seeing the command and its spend, is. The one exception (khn3r, operator ruling 2026-09-23) is the operator's own typed /cyberine:autopilot whose arguments name both an explicit scope (task ids, --area, or fleet-only --resume / --test-debug-fix / a goal) and an explicit budget (--max-iter, --max-sessions, --max-hours): that invocation is itself the yes, so after a green dry-run the skill starts it with --yes and no further question. A command the session composes itself, or a hand-off from /cyberine:help, never qualifies; those get the skill's single question round.
/cyberine:help (added 2026-09-18, k7dad) is the one model-invocable plugin skill on a spend-adjacent
surface: a plain-language front door that maps a request anchored on the
word "cyberine" onto the right leaf skill (init, tasks, plan, ask, run, autopilot,
research), asks exactly one question on a genuine tie (a bare "cyberine brainstorm" between plan
and ask), and states what a leaf would spend before handing off — it never dispatches anything
itself. Known limitation: the anchor wording is a design self-check against the one failure case
named when the operator disabled model invocation on the other five skills (2026-09-13), not an
empirically verified filter — plugin-evals/help-routes-negative-no-anchor/ is the real
falsification test and has not yet been run (it spends quota, operator's call). Sibling-skill
reachability (can help Read and follow a disabled leaf's SKILL.md) was confirmed at the
filesystem level for both the installed symlink and --plugin-dir, not on a live paid run.
Evaluate the plugin with claude plugin eval
Claude Code 2.1.269 added claude plugin eval: it runs each case under the plugin's evals/ in a fresh isolated session with only the Cyberine plugin loaded (and, by default, once more with no plugin as a baseline), then scores the result with the case's graders and writes a JSON + HTML report. The suite ships init-readiness (an empty workspace must come back as a gap report with a fix per gap and who runs it), run-discovery (a health question must run cyberine fleet doctor and never dispatch), run-refuses-undeclared (a seat name fleet doctor never printed must be refused, not dispatched), and help-routes-* (added 2026-09-18, k7dad: ten cases covering every leaf the help router maps to, the router answering "which command" directly, one genuine disambiguation tie, and the required negative case where a Cyberine-less prompt must invoke nothing), and the natural-language-first acceptance suite nl-* (added 2026-09-18, kg3gm: 57 single-turn plain-language cases, 19 intents x 3 phrasing styles -- Vietnamese without diacritics, Vietnamese with diacritics, English -- covering init, resume, capture, handoff, plan, ask, run, autopilot, research, five CLI-only intents reached through the cheatsheet (cost, usage, digest, apply, doctor), a plain "what's next" with no anchor in a repo that has a board (nl-board-*, kfp69: must reach tasks), and four negative groups: bare "brainstorm", bare "is this worth it?", the same plain board question in a directory with no board (nl-neg-noboard-*), and a Cyberine-anchored code review that Claude Code must do itself rather than send to cyberine run; every case asserts no spending command runs before a yes).
bash plugin-evals/_harness/run.sh --ablation none --runs 1 # cheapest loop while editing cases
bash plugin-evals/_harness/run.sh # full: 3 runs per case, with-vs-without baseline
bash plugin-evals/_harness/run.sh --case 'run-*' --judge-model sonnet
bash plugin-evals/_harness/run.sh --case 'help-routes-*' --judge-model sonnet
bash plugin-evals/_harness/run.sh --case 'nl-*' --ablation none --runs 1 --judge-model sonnet --no-publish --max-cost-usd 30 -j 4
claude plugin eval init --bare <case-name> --eval-dir evals # then move the new case dir into plugin-evals/--allow-tools Bash is required: every bundled skill drives the cyberine CLI; add ,Skill when a case's prompt is plain language and depends on model invocation (every help-routes-* case). Results land in plugin-evals/results/<timestamp>/ (gitignored); exit 0 when every case meets --threshold (default 1.0), 1 on a failing case, 2 on a partial run. Case format: prompt.md (frontmatter max_turns, timeout_seconds, tags, allowed_tools; body is the user prompt), graders/*.md (frontmatter type: regex|tool_used|tool_order|file_exists|llm|baseline, optional weight, arm). plugin-evals/README.md carries the per-case intent. The suite lives outside plugin/ since k6na8, so the directory Anthropic's plugin directory bundles carries no fixtures; claude plugin eval only looks for cases below the plugin root, so run.sh copies plugin/ into a temp dir, copies the suite in as its evals/ (results excluded), and points --output-dir/--report back at plugin-evals/results/. It already passes --scaffold --trust-plugin --allow-tools Bash,Skill and puts the replay cyberine stub on PATH.
Known Limitations and Deferred Work: every run spends the operator's Claude quota (cases x runs, doubled by the baseline arm, plus three judge calls per llm grader per run), so the suite is on-demand only and is NOT in CI or the sweep. Measured 2026-09-13 (four runs, about $3 total): (1) the eval sandbox mounts only the resolved plugin directory and a throwaway HOME, so the cyberine binary (a symlink into this checkout's bin/, outside plugin/) is Operation not permitted and ~/.cyberine is empty -- every case therefore grades the skill's DISCIPLINE when the CLI is unavailable (report what the command returned or that it failed, never invent a seat, never dispatch), not a CLI happy path; (2) the bundled skills are disable-model-invocation: true, so a plain-language prompt never fires them -- prompts are written in slash form (/cyberine:init), which the runner expands, and that path does NOT go through the Skill tool, so tool_used: Skill graders read 0 by construction and were dropped in favour of behaviour graders; (3) a tool_used guard for "never runs X" needs BOTH min: 0 and max: 0 (the default min is 1, so max: 0 alone can never pass) and its input_match sees the whole JSON input including the Bash description, so anchor it on the command field -- and allow cyberine as the FIRST token: until 2026-09-18 (kg3gm) the shared no-dispatch pattern demanded a separator character before cyberine, so a bare cyberine run "..." never matched and those guards could not fail; every guard now uses the no-spend-before-yes pattern from the nl-* suite. The nl-* suite measures the --plugin-dir plugin load (the runner passes the target path to every child as --plugin-dir), not the ~/.claude/skills/cyberine symlink install, and is estimated at about $15 to $25 per single-arm pass over its 57 cases; plugin-evals/README.md carries its full matrix, run commands and blind spots. Final state: all three cases at 1.0 on the with-arm (runs e + f); the no-plugin baseline arm (Δ) has not been run yet (about $2 more). autopilot, brainstorm and decide have no case yet. The bundled MCP server is never started by a run and has no mock files under evals/mocks/cyberine/. The runner refuses any Bash-granting eval while a credentials config it scans carries what it reads as a relative path -- on this machine that was an Augment-installed ssh Include whose ProxyCommand -CAfile <(...) tripped it; the error names no file.
Plugin hooks: the task-board banner and guard only (2026-09-29)
The plugin ships exactly two hooks, both in plugin/hooks/ (hooks.json runs board-hooks.mjs):
- SessionStart banner (
startup|resume|clear): runs thecyberine-tasks-bannerlauncher that sits beside thecyberineon PATH, so a session opens with the board's counts andResume:line. It prints nothing in a repo without a board. - Visible on screen (2026-10-01). Both SessionStart hooks (the banner and the handoff pointer below) answer with one JSON object:
systemMessagecarries the first line with its trailing absolute path cut, which Claude Code renders under its startup header asSessionStart:startup says: [cyberine tasks] .../... [handoff] newest for ..., andhookSpecificOutput.additionalContextcarries the full text for the model. Plain stdout from a SessionStart hook reaches only the model, which is why the operator never saw these lines before. Measured on Claude Code 2.1.285 with three hooks showing at once (cyberine's two plus another plugin's);systemMessageis not in the documented SessionStart output fields, so a Claude Code upgrade that drops the line is the first suspect. Thecyberine handoff pointerhook thatcyberine initwrites into.claude/settings.local.jsonfor a repo without the plugin still prints plain text, so it stays model-only. - PreToolUse board-write guard (
Edit|Write|NotebookEdit|MultiEdit|Bash): refuses a hand edit of a board file, the same check ascyberine tasks guard(below). The hook script first checks the payload in-process and exits in about 30 ms unless it names a board file (ACTIVE.md,TASKS.md,status.d,journal, ...); only then does it boot the CLI, which takes about 2 s.
Both hooks do nothing when the cyberine CLI is not on PATH, and both always run, whether or not the standalone cyberine-taskboard plugin is also enabled: the big plugin replaces the standalone one, so it is the standalone plugin that yields to this one when it detects it, once its own update ships. Until then a machine with both installed gets a harmless double banner and double guard. scripts/plugin-check.sh fails on any other file in plugin/hooks/ or any hook command that does not run board-hooks.mjs, so no other hook can slip in.
echo '{"cwd":"'"$PWD"'"}' | node plugin/hooks/board-hooks.mjs banner # what SessionStart runs
echo '{"tool_name":"Write","tool_input":{"file_path":".local/tasks/ACTIVE.md"},"cwd":"'"$PWD"'"}' | node plugin/hooks/board-hooks.mjs guard; echo $? # 2Known Limitations and Deferred Work: hooks load only when the plugin is installed as a plugin (a marketplace install); a skills-directory symlink from cyberine plugin install may expose the skills without the hooks, which was not verified. A repo that already added the banner or guard by hand (the recipes below, or cyberine init --rules) runs it twice until the hand-added copy is removed. The marker filter looks for file NAMES in the raw payload, so a Bash command that merely mentions journal pays the 2 s CLI boot and is then allowed.
Handoff hook in the plugin (2026-09-30). The plugin also runs cyberine handoff pointer at SessionStart, through the same board-hooks.mjs (mode handoff-pointer), so the handoff pointer reaches every repo without a per-repo cyberine init step, which makes three hooks in all: the board banner (SessionStart), the board guard (PreToolUse) and the handoff pointer (SessionStart). It runs in every git repository (CLAUDE_PROJECT_DIR, else the working directory, walking up to the git root), with no cyberine init needed, only reads, and prints nothing when the repo has no handoff (it also shows a legacy .remember/handoffs/ one). It steps aside when that repo's own .claude/settings.local.json or .claude/settings.json already runs the same command, so a repo that installed it through an older cyberine init does not get it twice. cyberine init no longer offers the per-repo hook when the plugin is installed (the skills-directory symlink, or an enabled cyberine@* plugin); it prints one line saying the plugin runs it. The plugin does NOT run cyberine handoff capture at SessionEnd: it did for a few hours on 2026-09-30 and the hook was removed the same day, see "SessionEnd safety net (removed)" under "Session handoff". A repo whose .claude/settings.local.json still carries a SessionEnd entry from an older cyberine init should delete that entry.
echo '{}' | CLAUDE_PROJECT_DIR="$PWD" node plugin/hooks/board-hooks.mjs handoff-pointer # the [handoff] line, or nothing when the repo has no handoffThe 2026-09-21 ruling below still stands for everything else: no other hook ships in the plugin. The same script also parses every plugin/skills/*/SKILL.md frontmatter with the yaml package in strict mode (unique keys) and fails when one does not parse, when its name differs from its directory, or when its description is not a string of at most 1024 characters: Claude Code tolerates an unquoted : in a description, the plugin directory's review does not, and claude plugin validate --strict never reads skill frontmatter (kp0g5). It also caps every description at 48 characters and description plus when_to_use at 1536 (ktf2c): Claude Code's slash typeahead gives its list about five lines, so one description that wraps to two lines hides the other /cyberine:* commands (measured on 2.1.283: typing /cyberine showed two of ten). Trigger phrases for the two model-invocable skills, help and handoff, live in when_to_use, which the skill listing the model reads appends to the description and the typeahead does not show. Run it as bash scripts/plugin-check.sh.
Until 2026-09-21 one function-hook module shipped here, plugin/hooks/sender-trailer.ts, which appended a [sender] repo=... root=... remote=... branch=... cwd=... session=... line to every plain-text SendMessage a session sent to a peer. It was removed by operator ruling for two reasons, neither of which is about whether it worked:
- It is a personal convention, not an orchestrator feature. Knowing which repo a peer message came from is useful to one author running many sessions on one machine. It is not something a Cyberine user asked for.
- A plugin hook has no per-project opt-in. It fires in every repo on the machine. This one rewrote the body of a message the user wrote, and sent that user's absolute repo root, cwd and session id to whoever received it. The README used to justify shipping it as "harmless everywhere", which was true of the intent and not demonstrably true of the effect on someone else's machine.
Two defects measured on the way out, recorded because they are the reason "harmless" did not hold, and because any future line-rewriting hook will meet them again:
- There was no escape logic.
> [sender] ...survived only because>is a non-whitespace character ahead of the token;x[sender] ...survived identically. The check wasline.trimStart().startsWith('[sender]'). - Indentation did not protect a line, and that is the one someone actually hits.
trimStart()ran before the test, so a trailer quoted inside a Markdown list continuation, any indented block, or a fenced code block was silently relabelled to[sender-quoted]. The note the sender received called>"the escape", which got the outcome right and the mechanism wrong.
One property held up better than it was written to: publicRemote() never leaked credentials. Thirteen adversarial remotes (https://tok3n:x@, ghp_AAA@, ssh://ghp_AAA:x@host:22/, SCP-style with userinfo, file:, a bare path) all came back clean, in the SCP branch because the userinfo sits outside the capture group and in the URL branch because new URL exposes only hostname and pathname.
The author's personal copy now lives in ~/.claude, shipped off by default, where it is opt-in.
Known Limitations and Deferred Work: Cyberine's own team bus (send_message / send_to_team MCP tools) never carried this trailer and still does not; a peer message carries no sender provenance at all now, and a receiver identifies a sender by finding its live row in ListAgents. Nothing replaces it inside the plugin, and nothing should: if session-to-session provenance becomes an orchestrator feature rather than a personal one, it belongs on the bus, where it is opt-in per project and not tied to an early-access hook API.
Opt-in mod: argument suggestions for /cyberine:* (mods/cyberine-suggest/)
Typing /cyberine:<skill> normally shows only the skill's dim argument-hint. The cyberine-suggest mod draws that hint as a pickable list above the prompt instead: one row per alternative, and one row per option inside a [...] group (--depth quick, --depth deep, --depth max). Typing filters the list by prefix; after the first argument only flags you have not used yet are offered (/cyberine:plan ship v1 --d offers the three --depth rows). Picking a row fills the prompt up to the first placeholder (close <id> fills close ), and a row that is only a placeholder (<task>) shows dim and cannot be picked. It ships outside plugin/ because a plugin hook fires in every repo on the machine with no opt-in (see "No plugin hooks" above); this one runs only in sessions you load it into.
Run recipe, from a checkout:
claude --plugin-dir mods/cyberine-suggest # this session only
claude plugin validate mods/cyberine-suggest # what it hooks, calls and readsFor every session, put the absolute path in CLAUDE_CODE_PLUGIN_DIRS (the process environment or the env block of ~/.claude/settings.json). Pick a row with a click, or with ctrl+x tab to focus the list and Enter on the highlighted row.
Rows come from each skill's argument-hint in plugin/skills/*/SKILL.md, read once at session start from plugin/skills beside the mod, else from ~/.claude/skills/cyberine/skills (the cyberine plugin install symlink). A skill with no hint (init) or a parenthesised one (handoff) gets no list. To change what a skill offers, edit its argument-hint.
Known Limitations and Deferred Work: Up/Down at the prompt cannot be captured by a mod, so the arrows never walk the list; selection is a click, ctrl+x tab, or typing the full text. It depends on Claude Code's early-access function-hooks API (measured on 2.1.283) and reloads only on session start, so an edited argument-hint shows in the next session. It is not in the npm package (files carries plugin/, not mods/), so it works from a checkout or a copy of the folder. Rows come from argument-hint alone, not from the CLI surface in src/core/commandRegistry.ts: the skills take plain language, and a CLI flag the skill does not parse would be a wrong suggestion. Verified by hand in a private tmux (filtering, ctrl+x tab + Enter fill); a mouse click and the tui:e2e suite were not run against it.
Standalone task board bundle for the cyberine-taskboard plugin
The task board also ships on its own as the cyberine-taskboard marketplace plugin, which lives in a separate repo (cyberinecore/cc-task-board). That plugin carries no copy of the board's source: it vendors a zero-dependency ESM build (an entry file plus its split chunks) made from this repo's src/tasks/ and src/commands/tasksBoard.ts, so both tools read and write the same board format.
npm run taskboard:bundle -- --out <plugin-dir> --program "/cyberine-taskboard:tasks" --tag taskboard
# writes cli/tasks.mjs, cli/lib/chunk-*.mjs, cli/build.json, templates/tasks-board.html,
# references/board-voice.md, skills/tasks/SKILL.md, skills/end/SKILL.md
npm run taskboard:bundle -- --check <plugin-dir> # rebuilds with the options in cli/build.json, exit 1 naming each differing file
node <plugin-dir>/cli/tasks.mjs status --summary # the same board subcommands as `cyberine tasks`, minus `run`, `group` and `end --handoff`
node <plugin-dir>/cli/tasks.mjs next # runnable / blocked / waiting sets, like `cyberine next` without --dispatch
node <plugin-dir>/cli/tasks.mjs status --json # board-status/1, byte-identical to `cyberine tasks status --json`
node <plugin-dir>/cli/tasks.mjs next --json # board-next/1, byte-identical to `cyberine next --json`
node <plugin-dir>/cli/tasks.mjs --version # "<program> <package version> (source <sha>)"--program replaces cyberine tasks in every message, usage line and the HTML board, drops the .claude/rules/cyberine-board.md citation from the guard's refusal and points it at the bundled CLI instead; --tag sets the banner prefix ([taskboard] <repo> · ...). The guard's refusal calls the manifest "a TaskBoard board file", and the few other places the source names the full product (the submodule-scope notice, the CRLF notice, the empty-board hint, the HTML footer and its localStorage key) are rewritten to TaskBoard wording. Each rewrite must match at least once or the build fails, so a moved source string is caught at build time rather than shipped half-branded. --version names the last commit that touched any bundled input, with +dirty when one has uncommitted changes. suggest recommends next instead of run in the bundle, through the src/tasks/boardSurface.ts seam the build stubs out.
npm run taskboard:bundle -- --out <plugin-dir> # unbranded: messages keep saying `cyberine tasks`The build (scripts/build-taskboard-bundle.ts, esbuild pinned in devDependencies, target node 20) swaps two modules for stubs under scripts/taskboard-bundle/: packageRoot() resolves to the plugin root so board finds its HTML template, and src/commands/tasksBoardFleet.ts (the run and group subcommands, the end --handoff writer and their usage lines) is replaced by a stub, so none of that code or text reaches the bundle. In the bundle run and group are ordinary unknown subcommands (exit 2, usage text without them) and end --handoff, --model and --backend are ordinary unknown arguments of end; the standalone entry adds next and --version, which the full CLI keeps elsewhere (cyberine next, cyberine --version). The build fails when the bundle would import anything other than a node: builtin.
File size cap (TaskBoard portal hold, 2026-10-01). The plugin directory's portal reads each file only up to 256 KiB, and a single cli/tasks.mjs had grown past that (303,605 bytes), so part of it went unscanned. The build therefore uses esbuild code splitting: cli/tasks.mjs is the entry and cli/lib/chunk-<hash>.mjs holds the modules reached through the seams listed in SPLIT_SEAMS (today src/tasks/boardWrite.ts), about 177 KB and 131 KB. The seam is a build-time extra entry point whose own output is discarded, so src/ keeps its static imports and the full CLI is unchanged. Every generated file, text included, must be at most 256 KiB or the build fails naming the file; the fix is another seam in SPLIT_SEAMS. --out deletes cli/lib/ before writing, because a chunk's name changes whenever its content does. The process.env limit counts across all the JS files together.
Marketplace text scan (kxt2k). The plugin directory's portal scans every file for a download-and-run command and for a credential read near a URL, so the build asserts on its own output and throws before writing anything: no npm i / npm install string, no word "pass" (any case or inflection) directly before a backticked or $ token, which the portal reads as a credential forward, no @cyberine/cli, full Cyberine or cyberine run, for a branded build no mention of the full product at all (only the plugin name cyberine-taskboard, the --program text, the env names below and the four quoted detection literals of the yield check, BIG_PLUGIN_LITERALS in src/tasks/boardYield.ts, are allowed), no run, group or --handoff surface, and at most one process.env in the bundle. Every environment read of the board goes through the single allowlist in src/tasks/boardEnv.ts (BOARD_ENV_NAMES plus the one accessor boardEnv(name)), for the full CLI and the bundle alike, so behaviour is identical and the reviewable site is one file. The board root override for TaskBoard users is TASKBOARD_ROOT=<dir>; it is read first, and CYBERINE_TASKS_ROOT and NF_TASKS_ROOT stay honoured as silent aliases. The other allowlisted names are NF_TASKS_AGENT, NF_TASKS_MACHINE, NF_TASKS_SYNC_DIR, NF_TASKS_STALE_DOING_DAYS, NF_TASKS_SESSION, NF_TASKS_CLAIM_TTL_MS, CYBERINE_TASKS_NO_PROMOTE, CLAUDE_JOB_DIR, and, for the yield check below, CLAUDE_PROJECT_DIR and PATH. Adding a variable means adding it to that list first, or boardEnv throws.
One source for both plugins (k4hwz). Operator ruling 2026-09-29: this repo's plugin is the single source, and whatever both plugins emit (board files, banner and guard text, the reply rules) must be identical so either one is a safe fallback for the other on a shared board. The CLI half was already one build. For a --program shaped /<plugin>:tasks, the build now also emits the text half:
references/board-voice.md, derived fromplugin/references/board-voice.md, the reply rules (only the user's own message counts, talking to the user, consent, undo, dead ends, the user-facing commands) that this plugin'stasksandendskills read before their first reply. The two plugin-specific regions, marked<!-- variant:dead-ends -->and<!-- variant:commands -->in the source, take their TaskBoard wording fromscripts/taskboard-bundle/voice/<name>.md; a marker with no file, or a file with no marker, fails the build.skills/tasks/SKILL.mdandskills/end/SKILL.md, from the templatesscripts/taskboard-bundle/skills/tasks.mdandend.md. A template line{{shared:<skill>/<name>}}is replaced by the section between<!-- shared:<name> -->and<!-- /shared:<name> -->inplugin/skills/<skill>/SKILL.md(todayvoice,never-hand-edit,converge,closing,blocked,hard-rulesintasks, andinbox,judge,ask-writeinend). The TaskBoardendis the free wrap:tasks unlogged, one question, onesave,tasks end, no handoff and no model call.
Every generated text is rebranded the same way: /cyberine:tasks|init|end|help become /<plugin>:..., cyberine tasks: (the CLI's message prefix) becomes the program, cyberine tasks <sub> becomes tasks <sub> in prose and node "${CLAUDE_PLUGIN_ROOT}/cli/tasks.mjs" <sub> inside a fenced block, and "operator" becomes "user". Each generated skill's frontmatter must parse as strict YAML with the right name:, and every generated file goes through the same marketplace text scan as the bundle, so a shared section that names the full product fails the build instead of shipping. The build emits no text for an unbranded --program.
--check <plugin-dir> rebuilds every output (bundle and text) with the options recorded in cli/build.json and compares bytes; on a difference it prints differs: <file> -- line <n>: vendored "...", fresh "..." per file and exits 1. That is the drift check for the TaskBoard repo's CI: check out this repository at the commit cli/build.json's source names (the last commit that touched any bundled input or text source), npm ci, then run npm run taskboard:bundle -- --check <taskboard checkout>. A hand edit of any vendored file fails it, and so does a file under cli/lib/ that a fresh build does not produce (a stale chunk left behind by copying a build over an older one).
Board format version (k4hwz). A new manifest (cyberine tasks init, cyberine init, the bundle's init) carries <!-- board-format: 1 --> in its preamble; a manifest without the line is format 1, so every existing board keeps working and nothing adds the line to one. The number lives in BOARD_FORMAT_VERSION (src/tasks/boardFormat.ts). A build that finds a larger number refuses every write (manifest rewrite, status flip, journal, draft, relocate) before touching a file and exits 2, while reads keep working with a warning on stderr:
cyberine tasks: <manifest> is board format 2, newer than this build understands (1) -- nothing written; update the cyberine CLI to change this board. Reading it still works.
/cyberine-taskboard:tasks: <manifest> is board format 2, newer than this build understands (1) -- nothing written; update the TaskBoard plugin to change this board. Reading it still works.The TaskBoard wording comes from a build stub (scripts/taskboard-bundle/boardFormatHintStub.ts in place of src/tasks/boardFormatHint.ts), so the bundle never names the full CLI. The grammar is in docs/tasks-manifest.md section 3.
npm run selftest:board-parity # one scripted board session through the CLI and a fresh bundle: byte-identical board files,
# a mixed A-writes / B-reads-and-writes / A-reads round trip both ways, and the format refusalYield to the big plugin (ka7fx). The two plugins are not meant to be installed together, and when both are, TaskBoard's hooks go silent and the big plugin's stay, so the user sees the same banner and the same guard whichever runs. The bundle's banner and guard subcommands first call bigPluginHooksWouldRun(mode, env, home, cwd) (src/tasks/boardYield.ts) and, when it returns true, exit 0 with no output; the guard then lets the tool call through, because the big plugin's own guard is running. The decision is true exactly when both hold: (a) the big plugin is installed and enabled, meaning ~/.claude/skills/cyberine resolves to a directory whose .claude-plugin/plugin.json has "name": "cyberine", OR some enabledPlugins key starting with cyberine@ is true in the winning settings scope (~/.claude/settings.json, then <project>/.claude/settings.json, then <project>/.claude/settings.local.json, a later scope's value for the same key overriding an earlier one; cyberine-taskboard@* keys never match); and (b) the big plugin's hooks would find the CLI, by the same rule as plugin/hooks/board-hooks.mjs: the FIRST cyberine on PATH, resolved through symlinks, must have cyberine-tasks-banner.mjs (banner) or cyberine.mjs (guard) beside it. When (b) fails TaskBoard never yields, since the big plugin's hooks would then be inert and nobody would guard the board. <project> is CLAUDE_PROJECT_DIR when Claude Code sets it, else the working directory. The yield is silent by design (TaskBoard's text never names the other product) and lives only in the bundle entry (scripts/taskboard-bundle/entry.ts): cyberine tasks banner and cyberine tasks guard in the full CLI ARE the big plugin's path and never yield, and nothing under src/ imports boardYield.ts. Check it by hand against a scratch HOME and PATH:
node <plugin-dir>/cli/tasks.mjs banner; echo $? # silent + 0 when the big plugin's banner would run here
echo '{"tool_name":"Write","tool_input":{"file_path":".local/tasks/ACTIVE.md"}}' | node <plugin-dir>/cli/tasks.mjs guard; echo $? # 0 when yielding, 2 otherwise
npm run selftest:taskboard-yield # the decision table, plus parity with board-hooks.mjs on the same PATHKnown Limitations and Deferred Work: --program rewrites only the literal cyberine tasks plus the specific strings listed above, so a new source string that names the full product fails the build's marketplace check until a rewrite is added for it. The bundle drops run, group and end --handoff outright; a TaskBoard user cannot dispatch tasks or write a handoff from it. CYBERINE_TASKS_NO_PROMOTE keeps its name in the bundle because the allowlist mirrors the full CLI. --check compares bytes, so a vendored copy built with a different esbuild version reads as stale. The yield covers the hooks only: TaskBoard's tasks skill cannot yield, because skill frontmatter is static, so with both plugins enabled both skills stay listed. The yield reads the settings files and the plugin symlink, not Claude Code's live plugin state, so a plugin enabled by --plugin-dir, a managed (enterprise) settings scope, or a session started before a settings change is judged from the files alone. selftest:taskboard-yield covers the decision function and its parity with the hook script, not the built bundle. selftest:board-parity runs a freshly built bundle for board writes, reads and the format refusal; the banner, the guard refusal text and the run/group/end --handoff refusals are still verified by hand. Generated text (k4hwz): only the TaskBoard tasks and end skills and references/board-voice.md are generated; its init and help skills, hooks/hooks.json, README.md, CHANGELOG.md and the plugin manifest stay hand-maintained in the TaskBoard repo, and so do the taskboard-only parts of the two generated skills, which live here as templates (frontmatter and trigger phrases, "How to run the CLI", "What's next", the intent table, "Out of scope", and the end skill's wrap and report steps). The rebrand is literal substitution, so a shared section must be written in words both plugins can say; a phrase the substitution does not cover fails the marketplace scan rather than shipping. The TaskBoard tasks skill no longer steps aside for a phrase that names the full product, since it may not name it; with both plugins installed both skills answer the same plain phrases, which is safe because both write through the same CLI. The format stamp is written only by init; nf-tasks ignores the line, so it would still write a board of a newer format, and a future format bump has to stamp boards it rewrites for the refusal to trigger on them.
Export the plugin for Anthropic's directory -- npm run plugin:export (k8y3j)
The directory reviews a plugin without a hold only when the plugin sits at the root of its own public repo. plugin:export copies plugin/ to the root of a new directory, adds .claude-plugin/marketplace.json listing the plugin at ./, then runs the directory's file checks: fewer than 512 files, no symlinks, no .DS_Store, no top-level bin/, no file over 256 KiB unless it is an image or a font, a README of at least 40 words, a LICENSE, a .mcp.json that parses with pinned npx/uvx packages and https URLs, front matter in every SKILL.md, and claude plugin validate --strict.
npm run plugin:export -- --out /tmp/cyberine-plugin-repo # export, then check
npm run plugin:export -- --out <dir> --marketplace <name> --owner "<name>" # defaults: cyberine, Nia D Pham
npm run plugin:export -- --check <dir> # re-check an existing exportIt exits 1 on any failed check, naming each one, and exits 2 when --out is not empty. It never commits, pushes or submits.
Known Limitations and Deferred Work: the checks come from the 2026-09-26 reading of the directory's pre-submission list, and the portal's own validator may hold for more. Secret scanning, submodules, LFS, lockfile installs and a name that is already taken are not checked. Nothing keeps a public repo in sync yet. Creating that repo and submitting are the operator's steps (k8y3j).
Fallback: hosts without plugin support
If your Claude Code installation does not support plugins (older version), use these commands instead:
cyberine skill install && cyberine mcp install # copy the bundled skills, register the MCP server
# restart Claude Code (or /reload) so the new skills + MCP server show upFor upgrades, run cyberine skill install --upgrade to refresh stale skill copies. The mcp install
needs the claude CLI on PATH to register live; without it (or with --print), it prints the
mcpServers JSON block to paste in manually instead.
Developer checkout
npm install
npm run typecheck # tsc --noEmit, no network needed
npm link # put the global `cyberine` command on PATH (see "Global cyberine command"); this IS today's "install the CLI" step until kwpwz publishes to npm
cyberine build # snapshot HEAD into dist/artifact/ so the global CLI stops running your editor bufferThe built artifact — cyberine build (kka26)
npm link points the global cyberine at THIS checkout, so before kka26 the CLI every other project on the machine invoked was this repo's live, unsaved working tree: a half-written src/*.ts here crashed an unrelated session elsewhere, with a stack trace naming a repo that had nothing to do with it.
cyberine build snapshots the COMMITTED tree — git archive HEAD of the paths package.json's own files list declares, so the set can never drift from what ships — into dist/artifact/, and the shim runs that instead of src/.
cyberine build # snapshot HEAD -> dist/artifact/
cyberine build --status # which entry this process booted from, the artifact's sha, and whether it is behind HEAD
cyberine build --quiet # build without printing the sha (what the post-commit hook runs)
CYBERINE_DEV=1 cyberine ... # the documented opt-in: run straight from src/ while developing THIS repoResolution order in bin/cyberine.mjs (via bin/resolveEntry.mjs): CYBERINE_DEV=1 wins, else the artifact if one exists, else — when the package root has no .git, i.e. an npm install — its own src/ silently as entry package (k5jb5; the install is already an immutable snapshot), else src/ with a one-line stderr warning naming cyberine build. A fresh clone therefore still works before the first build.
dist/artifact is a symlink into dist/artifacts/<sha>-<t>, and that is load-bearing rather than cosmetic: an absent artifact is exactly what sends a caller back to src/, so a swap that briefly removes the path would reintroduce this bug in a narrow window. rename(2) over a symlink is one atomic syscall, so no observer ever sees the path missing. Concurrent builds (the post-commit hook racing a manual cyberine build) serialize on dist/.artifact-build.lock, and the last two versions are kept so a process still importing from the previous one keeps resolving.
The shim resolves that symlink to its real path before handing --tsconfig and the entry to tsx (kq9m5, 2026-09-17). tsx applies a tsconfig only to files its include matches, and it compares against each imported file's REAL path: with the symlinked dist/artifact/tsconfig.json, no .tsx file under dist/artifacts/<sha>-<t>/src matched, jsx: react-jsx never applied, and every Ink surface compiled to React.createElement -- cyberine chat exited with React is not defined from 2026-09-12 (kr40v's symlink swap) until this fix, while CYBERINE_DEV=1 kept working. A process resolves the path once at start, so a rebuild during its lifetime is picked up by the next invocation, the same as before.
.githooks/post-commit refreshes the snapshot after every commit, and .githooks/post-merge does the same after a git merge or git pull that lands without a manual commit (git runs only post-merge for those, so before this hook a clean merge left the global CLI on the pre-merge snapshot), so the global CLI tracks HEAD without anyone remembering to rebuild. The builder (bin/build-artifact.mjs) is plain ESM and imports nothing from src/, which is what lets it run even when the working tree does not parse; it never fails a commit, and CYBERINE_SKIP_ARTIFACT_BUILD=1 skips it.
Verified by npm run selftest:shim-artifact, whose second half is the real thing: it stands up a scratch package from git archive HEAD, builds its artifact, writes a genuine syntax error into that copy's src/core/config.ts, and asserts the shim still answers from an unrelated cwd — with CYBERINE_DEV=1 failing on the same file as the negative control. The atomicity claim is measured the same way rather than argued: a separate process polls the entry path every millisecond through a rebuild and must never see it missing. The JSX half compiles a probe .tsx inside the built artifact through the resolved tsconfig and requires the automatic runtime, with the symlinked tsconfig path producing React.createElement as the negative control.
Known limitations and deferred work. The artifact is a source snapshot that still transforms through tsx at run time — this is not a compile step, and startup cost is unchanged. A package installed from npm is not a git checkout, so cyberine build refuses there before running any git command and says so, and cyberine build --status reports entry: package with no artifact needed (k5jb5). The check is .git at the package root, not git rev-parse: an install directory can sit inside someone else's repo — under Homebrew node the global prefix is inside /opt/homebrew, Homebrew's own checkout — and asking git from there answers for that repo. The hook only fires on commit — a git checkout or git pull that moves HEAD leaves the artifact behind until the next commit, which cyberine build --status reports as stale. Keeping two versions bounds dist/ at roughly 16 MB; a process that idles longer than two rebuilds before its next dynamic import can still lose its version, which nothing detects today.
Single-binary build — npm run build:binary (k3bmc)
bun scripts/build-binary.ts bundles src/cyberine.ts with Bun (minified, no source map), then compiles the bundle into one standalone executable per target: cyberine-<os>-<arch> (.exe on Windows) under dist/binary/ (gitignored). The same binary is the MCP server: cyberine __entry mcp runs src/mcp.ts over stdio (ke4s0, which dropped the separate cyberine-mcp-<os>-<arch> binary and halved every platform package). No Node and no node_modules are needed to run them. The build embeds what the source tree otherwise reads from disk: the package version (so --version, the TUI brand line and the MCP serverInfo stop printing unknown) and templates/rules/* (so init --rules works), through a generated src/core/buildInfo.ts. It stubs Ink's optional react-devtools-core, which is not installed. Inside a binary, every background path that normally respawns TypeScript through tsx (run --bg, the detached autopilot, background research, carry-offload) respawns the binary itself instead (src/core/selfSpawn.ts, internal __entry <name> argv).
npm run build:binary # the host target only
bun scripts/build-binary.ts bun-darwin-arm64 bun-linux-x64 # named targets
bun scripts/build-binary.ts --out /tmp/cyb bun-linux-arm64 # another output directory
./dist/binary/cyberine-darwin-arm64 --versionVerify it: npm run selftest:binary-build builds the host binaries into a temp dir, checks --version against package.json, runs init --rules in a fresh repo from the binary, refuses an unknown internal entry, and sends MCP initialize to cyberine __entry mcp. It exits 78 when bun is not installed.
Measured by the kyy92 spike (.local/research/2026-09-27-bun-compile-spike.m3max.md): about 68 MB on darwin-arm64 and 112 MB on linux-x64, --help in about 255 ms against about 1680 ms through the tsx shim.
Release tarball gate (k0q29). npm run release:gate -- <package.tgz> [more.tgz ...] [--max-file-mb N] unpacks each tarball npm pack produced and refuses it (exit 1, one line per finding, "publish nothing") when it carries a *.map, a JSON "sourcesContent": [...] array in any text file, a .ts/.tsx/.mts/.cts source (declarations .d.ts excepted), a src/ directory, a file over --max-file-mb (default 200), or a storage-bucket URL (S3, GCS, Azure blob, R2) in a text file. Compiled binaries are skipped by the content rules, because the Bun runtime inside every binary legitimately carries a source-map string and an S3 XML namespace; build-binary.ts runs the same checks on the JavaScript bundle before it compiles anything, which is where a leak would enter. Publish only the tarball the gate passed: npm pack, npm run release:gate -- <file>.tgz, then npm publish <file>.tgz, never npm publish from the directory. npm run selftest:tarball-gate covers every rule on probe tarballs.
npm pack --json > .local/tmp/pack.json && npm run release:gate -- cyberine-0.2.0.tgzThe current package is refused by design: it still ships src/ as TypeScript, which is what the per-platform binary packages will replace.
Known Limitations and Deferred Work. Not distributed yet: the per-platform npm packages and launcher are built by npm run release:packages below, the publish itself is kwpwz, and the gate is not in CI, because CI builds no release tarball. cyberine web still serves its dashboard from src/web/public/ on disk, so it does not work from a binary. Anything that resolves packageRoot() to persist a path (the plugin symlink, skill install, the launchd plist) still assumes a checkout or an npm install. The binary hides the file layout and the TypeScript, not the logic: the bundled JavaScript and its strings are extractable.
npm packages — npm run release:packages (ke4s0)
scripts/build-npm-packages.ts turns the binaries into the packages a user installs with npm i -g @cyberine/cli, the esbuild pattern: one per-platform package per target (@cyberine/cli-darwin-arm64, -darwin-x64, -linux-x64, -linux-arm64, -win32-x64, each carrying only bin/cyberine with os and cpu set), and a small Node launcher @cyberine/cli that lists all five as exact-version optionalDependencies, so npm installs only the one matching the machine. The launcher (packaging/npm/launcher/) carries the four bins (cyberine, cyberine-mcp = the binary's __entry mcp, cyberine-handoff = cyberine handoff, cyberine-tasks-banner), plugin/ and templates/, and exports CYBERINE_PKG_ROOT as its own directory, so cyberine plugin install links <prefix>/lib/node_modules/@cyberine/cli/plugin; the plugin's .mcp.json starts the cyberine-mcp bin the install put on PATH. The script builds every binary, stages every package under dist/npm/packages/, packs each into dist/npm/tarballs/, and runs the release tarball gate on every tarball; any finding exits 1 and means publish nothing. The version is package.json's, and the embedded package name the update check queries is @cyberine/cli. The repo's own root package stays cyberine and is never what gets published.
npm run release:packages # all five platforms plus the launcher, into dist/npm/
npm run release:packages -- --host # this machine's platform only
npm run release:packages -- linux-x64 darwin-arm64 # named platforms
npm run release:packages -- --host --out /tmp/cyb-npm # another output directoryPublish from the tarballs this printed, never from a directory: every platform package first, then the launcher (npm publish <file>.tgz --access public --otp <code>, the runbook under .local/npm/).
| Variable | Default | Meaning |
|---|---|---|
CYBERINE_BINARY_DIR | (unset) | launcher only: run <dir>/cyberine (cyberine.exe on Windows) instead of the installed platform package's binary |
Verify it: npm run selftest:npm-packages checks the launcher's platform map and its unsupported-platform and CYBERINE_BINARY_DIR paths, builds the host platform package and the launcher into a temp dir, asserts both pass the gate and that the launcher pins every platform package at this version, installs both tarballs into a temp prefix with a clean HOME, and runs cyberine --version, MCP initialize through cyberine-mcp and through the server a copy of the installed plugin's .mcp.json names, cyberine-handoff --help and init --rules; its negative control removes the platform package and expects the launcher to exit 1 naming it. It exits 78 without bun or on Windows.
Measured 2026-09-28 on m3max (Bun 1.2.19, 0.2.0): compressed tarballs of 23.2 MB (darwin-arm64), 25.5 MB (darwin-x64), 37.9 MB (linux-arm64), 39.3 MB (linux-x64), 41.5 MB (win32-x64) and 0.1 MB (launcher); all six passed the gate. Only the darwin-arm64 install was run.
Every x64 target is compiled on Bun's -baseline runtime (bun-darwin-x64-baseline, bun-linux-x64-baseline, bun-windows-x64-baseline; bunTarget() in scripts/build-npm-packages.ts), so no binary assumes AVX. darwin-x64 is additionally compiled with a pinned Bun 1.4.2 (npx -y bun@1.4.2 build, so the first build needs the registry) whenever the Bun running the script is older than 1.4: measured 2026-09-28, every Bun from 1.2.19 to 1.3.14 prints CPU lacks AVX support on each start under Rosetta even with -baseline, and 1.4.2 is clean. The other targets stay on the Bun you run, because a Bun 1.4.2 linux-x64 binary (either variant) aborted under QEMU where 1.2.19 to 1.3.14 ran; the same measurement rebuilt all six tarballs (darwin-x64 30 MB, win32-x64 42.7 MB) and ran darwin-x64 under Rosetta and linux-x64 on node:22-bookworm under QEMU with no warning.
Known Limitations and Deferred Work. Published as 0.2.0 on 2026-09-28 (kwpwz); a republish needs a version bump and one npm OTP per package. The win32-x64 binary is built but has never run. Linux packages are glibc builds; no musl (Alpine) package exists, so on Alpine npm installs the glibc linux package and the launcher refuses it by name (exit 1, measured on node:22-alpine arm64 and amd64); CYBERINE_BINARY_DIR still runs a binary you built yourself. Bun does offer bun-linux-x64-musl / bun-linux-arm64-musl targets, so musl packages are possible later and are deferred. A Bun 1.4.x linux-x64 binary aborts under QEMU; whether it also fails on real x64 hardware is untested, which is why linux stays on the Bun that runs the script. cyberine web does not work from a binary (its dashboard is read from src/web/public/). The launcher needs Node to start, like every npm bin. Cross-target builds download each target's Bun runtime on first use into ~/.bun/install/cache/bun-<os>-<arch>[-baseline]-v<bun version>; on 2026-09-28 that download crawled for bun-windows-x64 and hung the build, and copying bin/bun.exe from npm's @oven/bun-windows-x64 (now @oven/bun-windows-x64-baseline) tarball of the same Bun version into that cache path fixed it. npm's registry size limit for a 41 MB tarball was not checked.