{"record":{"id":"ac8ef9aacf0286ae","repo":"NousResearch/hermes-agent","slug":"invalid-cron-job-id-for-output-path-job-id-r","errorCode":null,"errorMessage":"Invalid cron job id for output path: {job_id!r}","messagePattern":"Invalid cron job id for output path: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"cron/jobs.py","lineNumber":391,"sourceCode":"\n# Fields on a cron job that must never change after creation. ``id`` is used\n# as a filesystem path component under ``OUTPUT_DIR``; allowing it to be\n# updated lets an unsafe value (``../escape``, absolute path, nested) leak\n# into output writes/deletes.\n_IMMUTABLE_JOB_FIELDS = frozenset({\"id\"})\n\n\ndef _job_output_dir(job_id: str) -> Path:\n    \"\"\"Resolve a job's output directory, rejecting any path-escape attempt.\n\n    Job IDs are filesystem path components under ``OUTPUT_DIR``. A legacy or\n    crafted ID containing ``..``, absolute paths, or nested separators would\n    allow output writes/deletes to escape the cron output sandbox. Reject\n    anything that isn't a single safe path component.\n    \"\"\"\n    text = str(job_id or \"\").strip()\n    if not text or text in {\".\", \"..\"} or \"/\" in text or \"\\\\\" in text:\n        raise ValueError(f\"Invalid cron job id for output path: {job_id!r}\")\n    if Path(text).is_absolute() or Path(text).drive:\n        raise ValueError(f\"Invalid cron job id for output path: {job_id!r}\")\n    return _current_cron_store().output_dir / text\n\n\ndef _normalize_skill_list(skill: Optional[str] = None, skills: Optional[Any] = None) -> List[str]:\n    \"\"\"Normalize legacy/single-skill and multi-skill inputs into a unique ordered list.\"\"\"\n    if skills is None:\n        raw_items = [skill] if skill else []\n    elif isinstance(skills, str):\n        raw_items = [skills]\n    else:\n        raw_items = list(skills)\n\n    normalized: List[str] = []\n    for item in raw_items:\n        text = str(item or \"\").strip()\n        if text and text not in normalized:","sourceCodeStart":373,"sourceCodeEnd":409,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/cron/jobs.py#L373-L409","documentation":"Security ValueError from _job_output_dir in cron/jobs.py: the job id is used directly as a filesystem path component under the cron OUTPUT_DIR, so ids containing '/', '\\\\', '.', '..', that are empty, or that are absolute/UNC/drive-prefixed are rejected to prevent output writes and deletes from escaping the cron output sandbox. This variant catches separators, empty/dot ids.","triggerScenarios":"Calling a job-output API (list/save/delete output) with job_id='../hermes', job_id='a/b', job_id='', or job_id='.' — typically from hand-crafted input, a migrated legacy store with unusual ids, or unvalidated caller data.","commonSituations":"Passing a composite key or file path as the job id; a client joining values with '/'; legacy jobs created before ids were constrained; fuzzing/probing attempts on the path boundary.","solutions":["Pass the plain alphanumeric job id (as returned by create_job), not a path or composite string","Sanitize before calling: reject ids containing os.sep, '..', or empty strings","If a legacy store really contains such ids, re-create those jobs to obtain clean ids"],"exampleFix":"# before\njobs.get_job_output('../config')\n# ValueError: Invalid cron job id for output path: '../config'\n\n# after\njobs.get_job_output('a1b2c3d4')","handlingStrategy":"validation","validationCode":"import re\n\n_SAFE_ID = re.compile(r\"^[A-Za-z0-9_-]+$\")\n\ndef is_safe_job_id(job_id: str) -> bool:\n    t = str(job_id or \"\").strip()\n    return bool(_SAFE_ID.match(t)) and t not in {\".\", \"..\"}","typeGuard":"from pathlib import Path\n\ndef is_safe_job_id(job_id: str) -> bool:\n    t = str(job_id or \"\").strip()\n    if not t or t in {\".\", \"..\"} or \"/\" in t or \"\\\\\" in t:\n        return False\n    return not (Path(t).is_absolute() or Path(t).drive)","tryCatchPattern":"try:\n    out = jobs.get_job_output(job_id)\nexcept ValueError as e:\n    if str(e).startswith(\"Invalid cron job id\"):\n        raise KeyError(f\"no such job: {job_id!r} (bad id)\") from e\n    raise","preventionTips":["Treat job ids as opaque tokens from create_job; never build them from paths","Validate ids with a strict ^[A-Za-z0-9_-]+$ pattern before any output call","Reject '..' and separator-containing ids at the API boundary of your own tooling"],"tags":["cron","security","path-traversal","validation"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}