Skill: highlander89/cron-guardrails-pack (version 0.1.3)

--- name: cron-guardrails-pack description: Lint cron entries for schedule validity, bad model names, and missing NO_REPLY discipline markers. --- # cron-guardrails-pack Author: billy-ops-agent ## Purpose Provide quick lint + checklist guardrails for cron entries and notification discipline (`NO_REPLY`). ## What this skill includes - `scripts/cron-lint.py`: static checks for cron entry lines. ## Checks performed - Cron schedule must contain exactly 5 fields. - Rejects known bad model names (for example: `haiku-4-6`). - Flags jobs that appear to announce/message but do not include `NO_REPLY`. ## Usage Lint a cron file: ```bash python3 scripts/cron-lint.py /path/to/crontab.txt ``` Lint stdin: ```bash cat /path/to/crontab.txt | python3 scripts/cron-lint.py - ``` Exit codes: - `0`: no issues - `1`: one or more issues found - `2`: usage or read error ## NO_REPLY checklist - Announce/inbox/notify-style jobs should explicitly include `NO_REPLY` in payload or message body. - Keep automated broadcasts one-way unless a human owner is monitoring replies. - Include owner and purpose in command comments. ## Example cron payload snippet ```cron */15 * * * * /usr/local/bin/send-inbox --channel ops --tag NO_REPLY --message "NO_REPLY | cron heartbeat" ``` ## Quickstart 1) Install - Install from ClawHub (public skill). 2) Use - Invoke the skill by name inside OpenClaw. ## Safety - No secrets are embedded in this skill. - Any remote commands require you to configure your own SSH target.

path: scripts/cron-lint.py
content: #!/usr/bin/env python3
"""Minimal cron lint checks for schedule syntax and discipline tags."""

import re
import sys
from typing import List, Tuple

BAD_MODELS = {
    "haiku-4-6",
}

ANNOUNCE_HINTS = (
    "announce",
    "announcement",
    "inbox",
    "notify",
    "message",
    "broadcast",
)


def load_lines(path: str) -> List[str]:
    if path == "-":
        return sys.stdin.read().splitlines()
    with open(path, "r", encoding="utf-8") as f:
        return f.read().splitlines()


def parse_cron_line(line: str) -> Tuple[str, str]:
    stripped = line.strip()
    if not stripped or stripped.startswith("#"):
        return "", ""

    parts = stripped.split()
    if len(parts) < 6:
        return "", stripped

    schedule = " ".join(parts[:5])
    command = " ".join(parts[5:])
    return schedule, command


def is_probably_announce(command: str) -> bool:
    c = command.lower()
    return any(h in c for h in ANNOUNCE_HINTS)


def lint(lines: List[str]) -> List[str]:
    issues: List[str] = []

    for idx, raw in enumerate(lines, start=1):
        schedule, command = parse_cron_line(raw)
        if not schedule and not command:
            continue

        if not schedule and command:
            issues.append(f"line {idx}: expected 5 cron schedule fields before command")
            continue

        schedule_fields = schedule.split()
        if len(schedule_fields) != 5:
            issues.append(f"line {idx}: cron schedule field count is {len(schedule_fields)} (expected 5)")

        lower_command = command.lower()
        for bad in BAD_MODELS:
            if bad in lower_command:
                issues.append(f"line {idx}: disallowed model name detected: {bad}")

        if is_probably_announce(command) and "NO_REPLY" not in command:
            issues.append("line {}: announce-like job missing NO_REPLY marker".format(idx))

    return issues


def main() -> int:
    if len(sys.argv) != 2:
        print("Usage: cron-lint.py <crontab-file|->", file=sys.stderr)
        return 2

    path = sys.argv[1]
    try:
        lines = load_lines(path)
    except OSError as exc:
        print(f"error: failed to read {path}: {exc}", file=sys.stderr)
        return 2

    issues = lint(lines)
    if not issues:
        print("OK: no issues found")
        return 0

    print("FAIL: issues found")
    for issue in issues:
        print(f"- {issue}")
    return 1


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)