CHR from the shell: quickchr CLI + centrs
SkillDev toolsAnswer a RouterOS question by asking RouterOS. quickchr boots a real, disposable MikroTik CHR router on the local machine under QEMU, no hardware, no cloud, gone when you remove it, and centrs drives it. Covers the shell path end to end: the add/start/stop/remove lifecycle and what --bg actually does, getting REST endpoints and credentials out of a machine, L2 links between two VMs, and the provisioning steps (device-mode) that only work before a machine's first boot. Use when: checking a claim or config against real RouterOS instead of guessing; needing a throwaway router for REST/CLI/API iteration; giving a shell script or CI job a router to talk to. For TypeScript harnesses that import the QuickCHR class, use routeros-quickchr instead.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the CHR from the shell: quickchr CLI + centrs skill
What this skill tells your AI
The instructions your AI receives, as published by tikoci/routeros-skills in routeros-quickchr-cli/SKILL.md and read by ahel’s review.
quickchr (@tikoci/quickchr, CLI + Bun/TS
library) downloads a MikroTik CHR image, boots it under QEMU, and hands you a
router. This skill is the shell-first path: you have a terminal, you want a
router to interrogate, and the answer will be quoted as shell + RouterOS
commands (e.g. in a forum.mikrotik.com reply). If you are writing a
bun:test harness that imports QuickCHR, use the
routeros-quickchr skill instead — one scheme per task, never both.1
Behavior below is pinned to quickchr 0.4.8. Several of these are 0.4.8 changes, called out inline; on 0.4.7 and earlier the answer differs.2
One limit to keep in view: a CHR booted this way runs the free
license (quickchr get <name> shows Level: free), which MikroTik caps
at 1 Mbps upload per interface — every other RouterOS feature is
unrestricted. Config, API and CLI grounding are unaffected; a throughput
or queue number measured here is the license talking, not the
feature.3
Lifecycle: add is not start
quickchr add --name lab-a --version 7.24.4 --arch x86 --add-network user
quickchr start lab-a --bg
quickchr exec lab-a "/system/resource/print"
quickchr stop lab-a
quickchr remove lab-a
addonly writes config (0s on a warm cache) and printsquickchr start <name>as its tip. It never boots — the single most common wrong assumption about this CLI.4start --bgblocks until the guest is REST-ready, then prints the REST/SSH/WinBox endpoints (≈22s first boot, ≈11s restart, warm-cache HVF). Under TCG software emulation expect minutes, not seconds — a sandboxed field lab saw ~4 min boots and a realBOOT_TIMEOUTat 480s. Size harness timeouts from the slow end.5exec <name> <command…>runs one RouterOS command over REST.--viaisauto|rest|qgahere, andqgais narrower than it looks: RouterOS only starts its guest agent for an x86 CHR under Linux KVM. Under macOS HVF or TCG the port is presented but the guest never opens it, and ARM64 CHR has no QGA at all — both time out. Stay on REST unless you are on Linux/KVM with an x86 guest. (The library'sExecTransporttype also listssshandconsole; those are not CLI surface.)stopis instant;removedeletes the machine.listshows state and PIDs.6- Version selection:
--version 7.24.4pins;--channel stable|long-term|testing|developmentresolves the newest of a channel. Barequickchr --versionprints what each channel currently resolves to.7 - Ports are auto-allocated from 9100 in per-machine blocks of ten
(9140, 9150, 9160 …), so parallel machines do not collide and you
need no port bookkeeping.
--port-baseoverrides;--forward name:host:guest/protoadds extras, ranges included.8 - Pre-warm without booting:
quickchr cache add --version 7.24.4 --arch x86resolves and downloads one image, no QEMU and no machine required.cache keyprintsdir=/version=/arch=for CI. (Both new in 0.4.8 — before it,cachewaslist|prune|clearonly and the only way to warm the cache was to boot something.)9 - Never hand-edit
machine.jsonto change networks or options — it breaks boot. Everything is reachable throughaddflags.10
--bg is a no-op — background it yourself
--bg changes nothing: background is already the default. The CLI
sets background = !wantFg, so the flag only fails to select the
foreground mode you did not ask for; --fg/--foreground is the
switch that does something. And start waits for REST-readiness by
contract either way, so --bg does not hand your shell back. Both
halves of what the name promises are things it does not do
(tikoci/quickchr#159 — the --no-wait half and the naming decision are
still open).11
Examples here pass --bg only because it is harmless and widespread in
existing scripts. To actually get the shell back, background it
yourself and poll readiness:
nohup quickchr start lab-a --bg >lab-a.start.log 2>&1 &
ready() { centrs retrieve --quickchr lab-a /system/resource >/dev/null 2>&1; }
for _ in $(seq 60); do ready && break; sleep 5; done # 60 x 5s = 5 min
ready || { quickchr list; tail -20 lab-a.start.log; exit 1; }
Bound the wait. start can fail outright — MISSING_QEMU,
SPAWN_FAILED, BOOT_TIMEOUT — and an unbounded until loop then
polls forever, which in CI is a hung job with no diagnostic. Size the
bound off the slow end (TCG minutes, not HVF seconds) and on timeout
print quickchr list plus the start log rather than just
exiting.12
A killed start does not stop the VM. On 0.4.8 QEMU is spawned
detached (its own session), so even a harness that SIGKILLs the whole
process group leaves the guest booting — it comes up REST-ready with
nothing left to report it. Verified against both published releases:
group-SIGKILL 8s into a first boot took the VM down on 0.4.7, and
left it running and REST-ready on 0.4.8. So never infer "start
failed" from a killed CLI — check quickchr list and poll REST, and
stop/remove to clean up. (A BOOT_TIMEOUT can likewise auto-clean
the machine while QEMU keeps running.)13
If a harness group-kills commands and you are pinned below 0.4.8, that kill does take the VM with it — upgrade rather than work around it.
NIC order and the two-VM link
- Put
userfirst:--add-network user --add-network 'socket::lab-link'. RouterOS auto-DHCPs only ether1 (observed: dynamic10.0.2.15/24), and the host port-forwards assume it.14 - For an L2 link between two VMs, prefer a named socket
socket::<name>. It resolves through a registry entry, defaults todgram(a unix datagram pair) off Windows, and either machine may start first — there are no listener/connector roles to get wrong.startprints the transport each link resolved to. Verified: two machines on onesocket::link, started in arbitrary order, static/24s on ether2, ping 3/3 on a macOS host.15 - The raw
socket:listen:<port>/socket:connect:<port>pair is the ordering-sensitive form — a TCP pair where the listener must start first. Reach for it only when you need those explicit roles;socket::<name>exists to avoid the ordering problem.16 - A third machine on a two-machine link is refused at
start(not atadd), naming the two holders and pointing at--mode mcast. That refusal is the configuration check working.17
Which transport: default dgram, opt-in mcast
quickchr networks sockets create <name> takes --mode dgram|listen-connect|mcast and prints the transport it created —
believe the output, not the name. Default is dgram (unix datagram
pair, filesystem-confined, two machines) on macOS/Linux and
listen-connect (TCP pair) on Windows, which has no AF_UNIX datagram
socket. --mode mcast is UDP multicast 230.0.0.1 — the only N-way
transport, and the only one that fails silently.18
quickchr prints all three hazards below as a warning when you create an
mcast link. Two of them fail silently — same signature each time:
interfaces up, addresses assigned, 100% loss, nothing logged:19
- No macOS delivery — QEMU's multicast socket omits
SO_REUSEPORT, which BSD/macOS require before two sockets on one group both receive (tikoci/quickchr#167). - Refused where unconnected UDP sends are blocked —
seccomp-filtered sandboxes and some Linux CI return
EPERMonsendto()while the group join succeeds (tikoci/quickchr#169).
The third is the opposite failure — traffic flows where it should not:
- Not confined to your machine — quickchr passes no
localaddr=, so the group rides the host's default multicast interface. Another host on your LAN using the same group joins your segment, and the link keeps working, so nothing looks wrong. That is an isolation failure, not a loss one. Give an N-way link a group of its own, or stay ondgramfor two machines.
Driving the router: centrs --quickchr
Once the machine is up, reads and writes go through centrs
(@tikoci/centrs) targeting the
machine by name — it resolves the endpoint through quickchr inspect,
so there is no credential plumbing:20
centrs retrieve --quickchr lab-a /system/resource
centrs execute --quickchr lab-a --yes '/ip/address/add interface=ether2 address=198.51.100.1/24'
Reads need no flag; writes refuse without --yes
([usage/confirmation-required]). One RouterOS statement per call.
Prefer it over quickchr exec: as of 0.4.8 quickchr itself says so, in
exec --help and in a stderr tip on a bare quickchr exec
(suppressible with QUICKCHR_NO_TIPS=1; tips never touch stdout, so
--json stays parseable). quickchr exec is a raw /rest/execute
pipe with no validation — whatever you type is what RouterOS is asked
to run, and its output is screen-scraped text where centrs returns
typed JSON.21
Need the raw endpoint instead — curl, or some other tool?
quickchr inspect <name> --json carries the resolved URL and
ready-to-use credentials. CHR here is admin with an empty
password, and inspect hands you the Basic header already encoded:
"rest-api": { "url": "http://127.0.0.1:9150/rest", "port": 9150,
"auth": { "username": "admin", "password": "", "basic": "admin:" } }
(auth also carries a pre-encoded header if you would rather not
build the Basic value yourself.) Straight to curl:
url=$(quickchr inspect lab-a --json | jq -r '.services["rest-api"].url')
curl -s -u admin: "$url/system/resource" | jq .
That is the same door centrs uses — --quickchr resolves through
quickchr inspect, never by reading machine.json — which is why it
needs no credential configuration from you.22
Device-mode: the window closes at first boot
Two traps, and agents hit them in this order.
The guest-side route does not work. /system/device-mode/update container=yes over REST or API does not complete — RouterOS is waiting
for a power cycle it cannot perform on itself — and the feature stays
false. Observed live: the call did not return within the 10s client
timeout and container was unchanged afterwards. (Whether it would
ever return is not established; a field lab reported waiting ~5 min.)
Do not reach for this, and do not read the timeout as a transport
problem.23
The flags are honored only on a machine's first boot. quickchr
applies device-mode by provisioning — boot the guest, set the mode,
power-cycle QEMU via the monitor, re-read to confirm (≈45s vs ≈22s
without). That runs only while the machine has never started. Once
lastStartedAt is set, --device-mode-enable is parsed, accepted and
silently ignored: no warning, no extra boot time, no change
(tikoci/quickchr#176 — open).24
| machine state | start --device-mode-enable container |
|---|---|
| added, never started | applies — "Device-mode verified: mode=rose container=yes" |
| started at least once | silently ignored — stays container: false |
So add --device-mode-enable <feature> when you can; if you only
realize you need it after creating the machine, you can still pass it
to the first start. Once it has booted, the CLI has no way to
change device-mode — remove + add --device-mode-enable … + start
is the only CLI path. (The library exposes
instance.setDeviceMode(), which power-cycles for you — see
routeros-quickchr.)
Which side of the line a machine is on, and what it currently has:
quickchr inspect <name> --json | jq -r '.lastStartedAt // "never started"'
quickchr get <name> # live Device Mode / License / Admin Users
Enabling container moves the mode itself too (advanced → rose).
Provisioning needs RouterOS 7.20.8+ and a user-mode NIC — with
only socket NICs it fails NETWORK_UNAVAILABLE; older 7.x is
boot-only.25
Grounding behind this skill
references/cli-grounding.md— the live runs (commands, outputs, timings, environment) and an explicit list of what was not re-verified there.- quickchr
MANUAL.md,DESIGN.md,docs/networking.md,CHANGELOG.md— authoritative behavior reference; the skill favors stable CLI concepts over version-specific flags. - Field lab that motivated this skill: external 3-CHR lab notes
(muse-app, 2026-09-19/20) —
add-doesn't-start, detached starts + REST polling, cold power-cycle for device-mode, cache-warming gap.
Footnotes
-
Split rationale: tikoci/routeros-skills#21. Library consumers stay on routeros-quickchr; shell-first agents come here. ↩
-
Pinned observations: published
@tikoci/quickchr0.4.8 (and 0.4.7 as a control), CHR 7.24.4 x86, QEMU 11.1.1 + HVF on macOS Intel. The named-socketdgramdefault, the printed transport, the three-machine refusal and the POSIX detach are all 0.4.8 (quickchr #158, #159) — they are absent from 0.4.7. ↩ -
MikroTik, CHR: Licensing → CHR License Levels: "The Free license level allows CHR to run indefinitely with a 1 Mbps upload limit per interface. All other RouterOS features are available without restrictions." Paid tiers raise the cap (P1 1 Gbit, P10 10 Gbit, P-Unlimited), and a 60-day trial of a paid tier is free with a MikroTik account. This is MikroTik licensing, not a quickchr behavior;
quickchr get <name>reportsLicense Level: freeon a default machine. ↩ -
references/cli-grounding.md§Lifecycle timings. ↩ -
Same; TCG band and the 480s
BOOT_TIMEOUTfrom the field lab notes. ↩ -
Same, §Lifecycle timings. ↩
-
quickchr add --help(--version,--channel);quickchr --versionprints the resolved stable/long-term versions. ↩ -
quickchr add --help— "--port-base <port>Starting port number (default: auto-allocated from 9100)"; successive machines observed at 9140/9150/9160 inreferences/cli-grounding.md. ↩ -
references/cli-grounding.md§Cache; quickchr CHANGELOG 0.4.8 "Added" (cache add,cache key). ↩ -
Field lab notes:
--add-networkataddtime, never a hand-editedmachine.json. ↩ -
--bgas a no-op:src/cli/index.ts— "Background default: true. Explicitly foreground only with --fg / --foreground / --no-background / --no-bg", andquickchr start --helprenders--bg / --background Run in background (default). tikoci/quickchr#159 — part 1 (--no-wait) and the--bgnaming decision remain open; part 2 (POSIX process group) shipped in 0.4.8. ↩ -
Failure codes from quickchr's
ErrorCodeunion (src/lib/types.ts); the bound-and-diagnose shape is this skill's recommendation, verified as a script inreferences/cli-grounding.md§Bounded readiness poll. ↩ -
references/cli-grounding.md§Group-kill, which records the 0.4.7-vs-0.4.8 before/after. ↩ -
references/cli-grounding.md§First NIC. ↩ -
references/cli-grounding.md§Named-socket link. ↩ -
parseSocketSpecifier()in quickchrsrc/lib/network.tsacceptssocket::<name>,socket:listen:<port>,socket:connect:<port>,socket:mcast:<group>:<port>;docs/networking.md— "Named sockets avoid the listen/connect ordering problem". ↩ -
references/cli-grounding.md§Third machine refused. ↩ -
references/cli-grounding.md§Socket create output;defaultSocketMode()in quickchrsrc/lib/socket-registry.ts. ↩ -
quickchr DESIGN.md "A named socket says what it is"; tikoci/quickchr#167 (delivery) and #169 (permission); warning text quoted in
references/cli-grounding.md. ↩ -
references/cli-grounding.md§Driving with centrs (retrieve,execute --yes, and the refusal without--yes). ↩ -
quickchr exec --helpand barequickchr execon 0.4.8, quoted inreferences/cli-grounding.md§Driving with centrs; CHANGELOG 0.4.8 "Added" (tips). ↩ -
references/cli-grounding.md§Endpoints and credentials (quickchr inspect --json,quickchr get). ↩ -
references/cli-grounding.md§Device-mode — the guest-side/system/device-mode/update container=yestiming out withcontainerunchanged, reproduced on 0.4.8 / CHR 7.24.4. ↩ -
Same section: the never-started machine applied and verified the change, the already-booted one silently did not. The gate is
!existing.lastStartedAtinQuickCHR.start()(src/lib/quickchr.ts), which passes provisioning options to_launchExisting()on a first boot andundefinedafterwards. Filed as tikoci/quickchr#176, which also asks which provisioning steps should be allowed to run later; until it lands, treat the silence as the documented behavior. ↩ -
quickchr MANUAL.md "Order of operations" (
_provisionInstance);--device-mode-enableinsrc/cli/flags.ts(accepted by bothaddandstart, though the condensedadd --helplists only--device-mode <m>); the user-mode-NIC requirement is theNETWORK_UNAVAILABLEguard inQuickCHR.start(). ↩
Signals
- GitHub stars
- 63
- Forks
- 15
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Key
routeros-quickchr-cli- Source
- github.com/tikoci/routeros-skills