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
- Check permissions and encoding: chmod +r plan.json; file plan.json to confirm UTF-8 text.
- Pass an absolute path so isfile() resolves predictably.
- 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
- chmod +r plan files created by other users/tools.
- Avoid passing inline JSON strings that collide with an existing filename in cwd.
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
- [CompetitorsPlan] Invalid JSON: {exc}\n
- [Planner] Cannot read --plan file: {exc}\n
- [last30days] Cannot read --synthesis-file: {exc}\n
- [CompetitorsPlan] Top-level must be a dict of {entity: {targ
- [Competitors] --competitors-list is empty.\n
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/9132aa2a2a49ec9d.
Report an issue: GitHub.