Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Pre-deployment checks for router and switch configuration, including dangerous commands, duplicate addresses, subnet overlaps, stale references, management-plane risk, and IOS-style security hygiene.
.claude/skills/network-config-validation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✗ | = Same ✗ | — | — |
| case-13 | ✗→✗ | = Same ✗ | — | — |
| case-12 | ✗→✗ | = Same ✗ | — | — |
| case-19 | ✗→✗ | = Same ✗ | — | — |
Use this skill to review network configuration before a change window or before an automation run touches production devices.
but not defined.
Treat config validation as layered evidence, not as a complete parser. Regex checks are useful for pre-flight warnings, but final approval still needs a network engineer to review intent, platform syntax, and rollback steps.
Validate in this order:
pythonimport re DANGEROUS_PATTERNS: list[tuple[re.Pattern[str], str]] = [ (re.compile(r"\breload\b", re.I), "reload causes downtime"), (re.compile(r"\berase\s+(startup|nvram|flash)", re.I), "erases persistent storage"), (re.compile(r"\bformat\b", re.I), "formats a device filesystem"), (re.compile(r"\bno\s+router\s+(bgp|ospf|eigrp)\b", re.I), "removes a routing process"), (re.compile(r"\bno\s+interface\s+\S+", re.I), "removes interface configuration"), (re.compile(r"\baaa\s+new-model\b", re.I), "changes authentication behavior"), (re.compile(r"\bcrypto\s+key\s+(zeroize|generate)\b", re.I), "changes device SSH keys"), ] def find_dangerous_commands(lines: list[str]) -> list[dict[str, str | int]]: findings = [] for line_number, line in enumerate(lines, start=1): stripped = line.strip() for pattern, reason in DANGEROUS_PATTERNS: if pattern.search(stripped): findings.append({ "line": line_number, "command": stripped, "reason": reason, }) return findings
pythonimport ipaddress import re from collections import Counter IP_ADDRESS_RE = re.compile( r"^\s*ip address\s+" r"(?P<ip>\d{1,3}(?:\.\d{1,3}){3})\s+" r"(?P<mask>\d{1,3}(?:\.\d{1,3}){3})\b", re.I | re.M, ) def extract_interfaces(config: str) -> list[dict[str, str]]: results = [] current = None for line in config.splitlines(): if line.startswith("interface "): current = line.split(maxsplit=1)[1] continue match = IP_ADDRESS_RE.match(line) if current and match: ip = match.group("ip") mask = match.group("mask") network = ipaddress.ip_interface(f"{ip}/{mask}").network results.append({"interface": current, "ip": ip, "network": str(network)}) return results def find_duplicate_ips(config: str) -> list[str]: ips = [entry["ip"] for entry in extract_interfaces(config)] counts = Counter(ips) return sorted(ip for ip, count in counts.items() if count > 1) def find_subnet_overlaps(config: str) -> list[tuple[str, str]]: networks = [ipaddress.ip_network(entry["network"]) for entry in extract_interfaces(config)] overlaps = [] for index, left in enumerate(networks): for right in networks[index + 1:]: if left.overlaps(right): overlaps.append((str(left), str(right))) return overlaps
Parse VTY blocks by section so access-class checks do not spill across unrelated lines.
pythonimport re def iter_blocks(config: str, starts_with: str) -> list[str]: blocks = [] current: list[str] = [] for line in config.splitlines(): if line.startswith(starts_with): if current: blocks.append("\n".join(current)) current = [line] continue if current: if line and not line.startswith(" "): blocks.append("\n".join(current)) current = [] else: current.append(line) if current: blocks.append("\n".join(current)) return blocks def check_vty_blocks(config: str) -> list[str]: issues = [] for block in iter_blocks(config, "line vty"): if re.search(r"transport\s+input\s+.*telnet", block, re.I): issues.append("VTY allows Telnet; require SSH only.") if not re.search(r"\baccess-class\s+\S+\s+in\b", block, re.I): issues.append("VTY block has no inbound access-class source restriction.") if not re.search(r"\bexec-timeout\s+\d+\s+\d+\b", block, re.I): issues.append("VTY block has no explicit exec-timeout.") return issues
pythonSECURITY_PATTERNS = [ (re.compile(r"\bsnmp-server community\s+(public|private)\b", re.I), "default SNMP community configured"), (re.compile(r"\bsnmp-server community\s+\S+", re.I), "SNMPv2 community string configured; prefer SNMPv3 authPriv"), (re.compile(r"\bip ssh version 1\b", re.I), "SSH version 1 enabled"), (re.compile(r"\benable password\b", re.I), "enable password is present; use enable secret"), (re.compile(r"\busername\s+\S+\s+password\b", re.I), "local username uses password instead of secret"), ] BEST_PRACTICE_PATTERNS = [ (re.compile(r"\bntp server\b", re.I), "NTP server"), (re.compile(r"\bservice timestamps\b", re.I), "log timestamps"), (re.compile(r"\blogging\s+\S+", re.I), "logging destination or buffer"), (re.compile(r"\bsnmp-server group\s+\S+\s+v3\s+priv\b", re.I), "SNMPv3 authPriv group"), (re.compile(r"\bbanner\s+(login|motd)\b", re.I), "login banner"), ] def check_security(config: str) -> list[str]: return [message for pattern, message in SECURITY_PATTERNS if pattern.search(config)] def check_missing_hygiene(config: str) -> list[str]: return [ f"Missing {description}" for pattern, description in BEST_PRACTICE_PATTERNS if not pattern.search(config) ]
change.
Use validation as a blocking gate before Netmiko, NAPALM, Ansible, or vendor API automation pushes a generated config. Fail closed on dangerous commands and credentials. Warn on best-practice gaps that are outside the change scope.
network-config-reviewernetwork-troubleshooternetwork-interface-health| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.