nexu-io/open-design · error · SystemExit

[CompetitorsPlan] Cannot read plan file: {exc}

Error message

[CompetitorsPlan] Cannot read plan file: {exc}

What it means

Inside the CompetitorsPlan sanitizer, when the `--plan` argument is an existing filesystem path (`os.path.isfile(plan_str)`), the loader opens and reads it. On OSError it writes `[CompetitorsPlan] Cannot read plan file: <exc>` to stderr and raises SystemExit(2). This fires only for the file-path branch; inline-JSON plans skip this check and go straight to json.loads (see 528).

Source

Thrown at design-templates/last30days/scripts/last30days.py:319


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:
            plan_str = open(plan_str).read()
        except OSError 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", "context",
    }
    normalized: dict[str, dict] = {}
    for entity, entry in parsed.items():
        if not isinstance(entry, dict):

View on GitHub (pinned to 5be4028344)

Solutions

  1. Verify the path is a readable regular file: `ls -l <path>` and `cat <path>`.
  2. Fix permissions: `chmod 644 <path>`.
  3. Pass the plan inline as a JSON string instead of a path (the loader detects non-path input and parses it directly).

Example fix

# before
python3.12 last30days.py --topic x --plan /secrets/plan.json   # mode 000
# after
chmod 644 /secrets/plan.json
python3.12 last30days.py --topic x --plan /secrets/plan.json
Defensive patterns

Strategy: validation

Validate before calling

import os
if os.path.isfile(raw):
    if not os.access(raw, os.R_OK):
        raise PermissionError(f'plan file not readable: {raw}')
    plan_str = open(raw).read()
else:
    plan_str = raw  # treat as inline JSON

Prevention

When it happens

Trigger: `--plan /path/to/plan.json` where the path exists as an entry (isfile returned something truthy enough to attempt) but read() fails — most commonly permission denied, or the path exists but is a broken symlink / special file that isfile fluked.

Common situations: Plan file chmod 000 or owned by another user; path is a dangling symlink; NFS/share hiccup; SELinux denying read.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/e01e7e59b6b03598. Report an issue: GitHub.