mvanhorn/last30days-skill · error · SystemExit

[CompetitorsPlan] Cannot read plan file: {exc}\n

Error message

[CompetitorsPlan] Cannot read plan file: {exc}\n

What it means

parse_competitors_plan() first treats the raw --competitors-plan value as a file path when os.path.isfile() matches; failures opening or decoding that file (OSError, UnicodeDecodeError) print a [CompetitorsPlan] message and exit 2. Note the value may also be inline JSON — the file branch only triggers when the string happens to name an existing file.

Source

Thrown at skills/last30days/scripts/last30days.py:803

def parse_competitors_plan(raw: str | None) -> dict[str, dict]:
    """Parse a --competitors-plan argument into a {entity_name_lower: plan_entry} dict.

    Accepts inline JSON or a file path (matches --plan). Returns {} on None/empty.
    Validation: top-level must be a dict; each value must be a dict. Unknown fields
    in entry values log a warning but do not abort. Invalid JSON or non-dict shape
    raises SystemExit(2) with a clear stderr message.
    """
    if not raw:
        return {}
    plan_str = raw
    if os.path.isfile(plan_str):
        try:
            with open(plan_str, encoding="utf-8") as f:
                plan_str = f.read()
        except (OSError, UnicodeDecodeError) as exc:
            sys.stderr.write(f"[CompetitorsPlan] Cannot read plan file: {exc}\n")
            raise SystemExit(2)
    try:
        parsed = json.loads(plan_str)
    except json.JSONDecodeError as exc:
        sys.stderr.write(f"[CompetitorsPlan] Invalid JSON: {exc}\n")
        raise SystemExit(2)
    if not isinstance(parsed, dict):
        sys.stderr.write(
            f"[CompetitorsPlan] Top-level must be a dict of "
            f"{{entity: {{targeting}}}}, got {type(parsed).__name__}\n"
        )
        raise SystemExit(2)
    known_fields = {
        "x_handle", "x_related", "subreddits",
        "github_user", "github_repos", "trustpilot_domain", "context",
    }
    normalized: dict[str, dict] = {}
    for entity, entry in parsed.items():
        if not isinstance(entry, dict):

View on GitHub (pinned to c7460f6114)

Solutions

  1. Check permissions and encoding: chmod +r plan.json; file plan.json to confirm UTF-8 text.
  2. Pass an absolute path so isfile() resolves predictably.
  3. If the value is inline JSON, ensure no file with that exact string as its name exists in cwd (which would hijack it into the file branch).

Example fix

# before
--competitors-plan ../plans/rivals.json   # relative, wrong cwd

# after
--competitors-plan /abs/path/plans/rivals.json
Defensive patterns

Strategy: validation

Validate before calling

p = Path(raw).expanduser()
if os.path.isfile(raw):
    data = p.read_bytes()
    data.decode('utf-8')  # raises before the engine does, with your own context

Prevention

When it happens

Trigger: --competitors-plan plan.json where plan.json lacks read permission, is a broken symlink (isfile False → falls to JSON parse instead), or contains non-UTF-8 bytes.

Common situations: Plan files written by another tool/user with 0600 perms; accidentally passing a binary or JSONL file; relative path resolving against a different cwd.

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/9132aa2a2a49ec9d. Report an issue: GitHub.