routeros-scripting — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited routeros-scripting (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
RouterOS scripts (.rsc, /system/script, scheduler on-event, Netwatch hooks, and interactive CLI snippets) use RouterOS's own language. It is not shell, Lua, Python, or Tcl. For the syntax baseline, see references/syntax.md. This skill focuses on the traps that LLMs are likely to confidently get wrong.
Interactive print output shows temporary row numbers (0, 1, 2...) for the current console buffer. They are not stable object IDs and are not usable inside scripts.
# WRONG in scripts — "1" is only an interactive print-buffer row number
/ip/route/set 1 gateway=3.3.3.3
# Better — internal IDs have a * prefix, but can change if objects are removed/re-added
/ip/route/set *1 gateway=3.3.3.3
# Best — resolve the target at runtime
/ip/route/set [find dst-address="0.0.0.0/0"] gateway=3.3.3.3For generated config, use stable selectors such as comment=, name=, or a unique address/list value. Internal IDs are visible in print as-value as .id=*HEX.
[find] Results as ID Arrays[find ...] returns zero, one, or many internal IDs. Many commands accept that array directly (set, remove, disable), but code that needs exactly one object should check the count before get.
:local ids [/interface/find where name="ether1"]
:if ([:len $ids] = 1) do={
:put [/interface/get ($ids->0) name]
} else={
:error ("expected exactly one interface, got " . [:len $ids])
}Use [find comment="my-tool-tag"] for idempotent cleanup, not broad selectors such as [find dynamic=no] that can delete unrelated user configuration.
find / whereRouterOS does aggressive type conversion, but not always in the direction you expect. For /ip/address, the address property is stored as a string including the prefix length. An unquoted literal like 111.111.1.1/24 is an IP-prefix value and may match nothing.
# WRONG — can silently return nothing
/ip/address/print where address=111.111.1.1/24
# Correct
/ip/address/print where address="111.111.1.1/24"
# Correct when value came from a variable
:local prefix 111.111.1.1/24
/ip/address/print where address=[:tostr $prefix]When a query unexpectedly returns nothing, inspect the stored type:
:put [:typeof ([:pick [/ip/address/print as-value] 0]->"address")]print as-value Is Usually an Array of Mapsprint as-value where ... returns {{...}; {...}}, even when only one row matches. Pick a row before accessing a property.
# WRONG — tries to read "gateway" from the outer array
:put ([/ip/route/print as-value where gateway="ether1"]->"gateway")
# Correct — pick the first map, then read the property
:put ([:pick [/ip/route/print as-value where gateway="ether1"] 0]->"gateway")For commands without get, as-value is often the only script-friendly output:
:put ([/tool/fetch url="https://example.com/file.txt" output=user as-value]->"data")once do={...}Commands such as monitor-traffic are interactive loops unless told to run once. Use once do={} to capture their temporary values in a script. Hyphenated field names use $"name-with-hyphens".
/interface/monitor-traffic ether1 once do={
:put ("rx=" . $"rx-bits-per-second")
}Use :log, not only :put, for scheduler or hook scripts where no terminal is attached.
Global variables and global functions are not automatically visible inside another script or function body. Declare access with :global name; before reading or calling them.
:global state "ready"
:global showState do={
:global state
:put ("state=" . $state)
}
:global addOne do={ :return ($1 + 1) }
:global caller do={
:global addOne
:return [$addOne 5]
}Use unique variable names. Avoid names that collide with RouterOS properties such as dst-address; if you must access such a variable, use quoted variable syntax ($"dst-address").
# Empty array: {} is a syntax error
:local arr [:toarray ""]
# Indexing and assignment use ->
:set ($arr->"name") "router1"
:put ($arr->"name")String concatenation with . distributes over arrays unless you convert first:
:local arr {"a"; "b"}
:put ("value=" . $arr) # two outputs: value=a and value=b
:put ("value=" . [:tostr $arr]) # one output: value=a;bNamed keys are sorted alphabetically. Unnamed elements preserve insertion order but are moved before keyed elements.
RouterOS has no direct "create empty file" or append primitive. The documented workaround creates a file via print file=..., then writes contents=.
/file/print file=myFile
/file/set myFile.txt contents=""To append, read the existing contents, concatenate, and write the whole file back. Large files and binary content are a poor fit for RouterOS scripts; prefer /tool/fetch, REST, SCP/SFTP, or external tooling when possible.
Scheduler, Netwatch, PPP on-up, and similar hooks execute with limited policies. If a script needs files, backups, passwords, sniffing, or reboot rights, the caller must have the matching policy.
/system/script/add name=backup policy=read,write,ftp,sensitive source={
/system/backup/save name=daily
}Be cautious with dont-require-permissions=yes: it allows less-privileged callers to run the script with the script's own policy set. Use it only for deliberately bounded scripts.
For unknown attributes, flags, and package-dependent commands:
<https://manual.mikrotik.com/docs/CLI%20Reference/>
/console/inspect is the groundtruth. See the routeros-command-tree skill.
rosetta MCP (routeros_explain_command,routeros_command_tree) for read-only explanation before execution.
quickchr exec <name> '<command>'on a disposable instance before publishing the script.
Confluence-based help.mikrotik.com documentation over time.
<https://manual.mikrotik.com/docs/Developer%20Guides/Scripting/scripting-tips-and-tricks/>
<https://manual.mikrotik.com/docs/Developer%20Guides/Scripting/scripting-examples/>
<https://manual.mikrotik.com/docs/CLI%20Reference/> — browsable command/property schema organized by package and command path.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.