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
- Verify the path is a readable regular file: `ls -l <path>` and `cat <path>`.
- Fix permissions: `chmod 644 <path>`.
- 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
- chmod 644 plan files.
- Prefer passing inline JSON for ephemeral plans to avoid FS perms.
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
- [last30days] Cannot read --synthesis-file: {exc}
- [CompetitorsPlan] Invalid JSON: {exc}
- [CompetitorsPlan] Top-level must be a dict of {entity: {targ
- [Competitors] --competitors-list is empty.\n
- [Competitors] --competitors must be >= {COMPETITORS_MIN} (go
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/e01e7e59b6b03598.
Report an issue: GitHub.