# Mikrotik Routeros

> Precise, version-specific MikroTik RouterOS CLI syntax. Use whenever writing, reviewing, or debugging RouterOS configuration, .rsc scripts, firewall rules, or any /ip /interface /routing /system command — and before handing any RouterOS config to a user or device. Also use for configuration drift between two RouterOS versions, upgrade impact, and "what changed / when did it change" questions.

- Skill: `joshfinlayau/mikrotik-routeros` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add joshfinlayau/mikrotik-routeros`
- Raw SKILL.md: https://api.skillmd.com/api/skills/joshfinlayau/mikrotik-routeros/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: JoshFinlayAU (https://skillmd.com/u/joshfinlayau)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/joshfinlayau/mikrotik-routeros

---


# MikroTik RouterOS

RouterOS CLI syntax has changed substantially across releases, and plausible-looking
commands are frequently wrong for the version actually running. This skill replaces
recall with lookup: a SQLite database harvested directly from live RouterOS devices
via `/console/inspect`, so every answer is ground truth for a specific version.

**Do not write RouterOS syntax from memory. Look it up, then lint it.**

## Workflow

### 1. Establish the version first

Every answer depends on it. If you have device access:

```
/system/resource/print
```

Take `version` (e.g. `7.16.1`) and `architecture-name`. Also useful, because the
command tree is package-dependent:

```
/system/package/print
```

If you cannot reach the device, **ask the user which RouterOS version they are
targeting**. Do not guess and do not default to "latest" silently.

### 2. Pick the query that matches the question

**Route on the question, not on habit.** Most of these are one command; reaching
for the wrong one turns a single lookup into a dozen.

| the question is... | use | not |
|---|---|---|
| "what are the arguments to X on version V" | `show` | |
| "what values does argument A take" | `arg` | |
| "where does X live in the tree" | `find` | |
| **"what changed between V1 and V2"** — drift, upgrade impact, "will my config still work" | **`diff`** | looping `show` over two versions |
| **"when did X change / which release introduced it"** | **`trace`** | looping `show` over every version |
| "what do we have data for" | `versions` | |

```bash
python3 scripts/ros_query.py show  /ip/firewall/filter/add --version 7.16.1
python3 scripts/ros_query.py arg   /ip/address/add interface --version 7.16.1
python3 scripts/ros_query.py find  address-list --version 7.16.1
python3 scripts/ros_query.py diff  7.13.5 7.23.2 --path /routing/isis
python3 scripts/ros_query.py trace /routing/isis/instance/add l1.redistribute
python3 scripts/ros_query.py versions
```

Add `--json` for machine-readable output.

**Two versions in the question means `diff` — run it first, before any `show`.**
Any question about upgrading, drift, "what's different", or config that works on
one version and not another is a `diff`, even when it is phrased as a question
about one command. Comparing `show` output by hand across versions is the wrong
tool: it misses enum value changes and `kind` changes entirely (those live inside
the argument, so both versions list the same argument name and look identical),
and it buries the real drift in console-wide plumbing. `diff` reports value-level
and kind-level changes and suppresses the plumbing; `show` cannot.

Scope `diff` with `--path` when the user named a subsystem. Unscoped is fine too
— it is fast — but the output is long.

`trace` answers "which release did this land in", which is what an upgrade path
actually needs: `diff 7.13.5 7.23.2` says a value is gone, `trace` says it went
at 7.19.4, so a hop to 7.18 is safe. Never answer "when did this change" by
running `show` against each version in turn.

### 3. Lint before delivering

This is not optional. Any config you produce — a full `.rsc` or a single line —
gets validated before it reaches the user or a device:

```bash
python3 scripts/ros_lint.py config.rsc --version 7.16.1
python3 scripts/ros_lint.py -c '/ip/address/add address=10.0.0.1/24 interface=ether1' -v 7.16.1
```

Exit code is non-zero when there are errors. Fix them and re-run. If the linter
reports an error you believe is wrong, say so explicitly to the user rather than
quietly ignoring it.

## Reading the results

Arguments are classified, and the classification changes how much you can trust
the value list:

| kind | meaning | how to treat it |
|---|---|---|
| `enum` | fixed set of values for this version | authoritative — anything else is wrong |
| `reference` | must name an object that exists on the device (`interface=`, `in-interface=`) | never invent a value; use one the user's config actually defines |
| `open` | free-form (addresses, comments, numbers) | not checkable from syntax alone |

Some enums are extensible — a firewall `chain=` may name a user-defined chain —
so the linter reports unknown values there as warnings, not errors.

## Reading drift output

`diff` separates three kinds of change, and they carry very different weight:

- **`arguments changed`** — an argument that exists in both versions but whose
  `kind` or accepted values moved. This is the dangerous category: the config
  line still parses, so nothing warns you, but the value you were passing may no
  longer be accepted. Read this section first.
- **`arguments added` / `paths added`** — new capability. Only matters when
  downgrading, or when you want the new feature.
- **`arguments removed` / `paths removed`** — config that will now fail outright.
  Loud, and therefore less dangerous than a silent value change.

`platform_wide_churn` is collapsed to counts because it is console plumbing
(`comments`, `group-by`, `on-event`, `proplist`, `follow-strict`) that changed
across hundreds of paths in the same release. It is never the answer to "what
changed in this subsystem". Pass `--include-noise` only if specifically asked.

### Non-monotonic changes are harvest artifacts, not release changes

A value or argument that disappears in one release and returns in the next was
never really removed. `/console/inspect` cannot distinguish a version-level enum
from a live object name, so a harvest taken on a configured router drops values
that the next clean harvest restores.

`trace` detects this and prints a `!!` warning. **Never report a flagged change
as real drift.** If `diff` shows a removal that looks surprising, `trace` that
argument before reporting it — the removal may be an artifact of a single dirty
harvest sitting between two clean ones. Cross-checking a sibling subsystem
(`/routing/ospf` for a `/routing/isis` finding) confirms it: if the same values
vanished there in the same release, it is the harvest, not RouterOS.

## Version fallback

If the exact version is not in the database, the closest one within the same major
version is used and **the substitution is reported in the output**. Always surface
that note to the user rather than presenting the answer as exact.

## RouterOS 6 is not covered

This database is **RouterOS 7 only**. RouterOS 6 lacks `/console/inspect`
entirely, so it cannot be harvested, and the tools refuse v6 queries rather than
answering from v7 data.

If the user is on RouterOS 6: say so plainly, and do not supply v7 syntax as a
substitute. Routing (`/routing bgp`, `/routing filter`, routing tables), wireless
(`/interface wireless` vs `/interface wifi`) and parts of `/ip` differ enough that
a v7 answer will be wrong in precisely the places that matter. Point them at
MikroTik's v6 documentation. `references/migration-6-to-7.md` describes the
differences, but its v6 claims are hand-written and unverified — treat it as
orientation, not ground truth.

## What the database does not cover

Introspection gives syntax, never semantics. For behaviour, ordering, and migration
traps, read the curated references — they hold what `/console/inspect` cannot tell you:

- `references/gotchas.md` — rule ordering, `place-before`, `[find]` vs numeric
  indices, safe-mode, and other ways syntactically valid config still misbehaves
- `references/migration-6-to-7.md` — what moved or was rewritten between v6 and v7
- `references/scripting.md` — the scripting language and its sharp edges

## Rebuilding the database

When a new RouterOS release appears, or to add a version:

```bash
python3 harvest/harvest.py --host <router-ip> --batch-size 40
python3 harvest/ingest.py --replace
```

`harvest/chr_lab.py` and `harvest/chr_proxmox.py` automate this against Cloud
Hosted Router images, so any released version can be harvested without hardware.

Harvesting a **zero-config** router (a freshly booted CHR) produces cleaner data
than a configured one: `/console/inspect` cannot distinguish a version-level enum
from a live object reference, so a device with lots of config pollutes value lists.
The ingester subtracts known live object names to compensate, but a clean baseline
is better.

