{"record":{"id":"9a85b56308d64f91","repo":"github/spec-kit","slug":"specify-event-command-event-failed","errorCode":null,"errorMessage":"specify event ${{command}} (${{event}}) failed: ${{(e as Error).message}}","messagePattern":"specify event (.+?)\\} \\((.+?)\\}\\) failed: (.+?)\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/specify_cli/events.py","lineNumber":443,"sourceCode":"  try {{\n    // execFileSync with an argv array invokes the interpreter directly — no\n    // shell — so command/event strings with metacharacters can't break out\n    // of the dispatcher argument (C9). The dispatcher arg is seconds; the\n    // execFileSync timeout is ms with a buffer so the outer cap fires after\n    // the dispatcher's inner subprocess (S3). stdout is captured and\n    // returned so context-injection hooks (experimental.chat.system.transform,\n    // chat.message) can push it into their outputs; stderr stays inherited so\n    // dispatcher errors remain visible (C11).\n    return execFileSync(INTERPRETER, [DISPATCHER, command, event, String(timeoutSec)], {{\n      input: JSON.stringify({{ input, output }}),\n      stdio: ['pipe', 'pipe', 'inherit'],\n      encoding: 'utf-8',\n      timeout: (timeoutSec + {buffer}) * 1000,\n    }});\n  }} catch (e) {{\n    // Propagate to OpenCode's hook machinery so only this hook is rejected,\n    // not the entire host process. process.exit() would kill the agent.\n    throw new Error(`specify event ${{command}} (${{event}}) failed: ${{(e as Error).message}}`);\n  }}\n}}\n\n// Cache session_start handler output per sessionID so non-idempotent\n// handlers (setup, telemetry, file-mutating scripts) run once per session\n// instead of on every LLM request (experimental.chat.system.transform\n// fires per LLM turn). Evicted on session.deleted.\nconst sessionStartCache = new Map<string, string>();\n\n{event_entries}\n\nexport default (async ({{ client, project, directory, $ }}) => {{\n  resolveDispatcher(directory);\n  return {{\n{plugin_returns}\n  }};\n}});\n'''","sourceCodeStart":425,"sourceCodeEnd":461,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/events.py#L425-L461","documentation":"Raised by _shared_relative_path() in specify_cli/shared_infra.py when a shared-infrastructure destination path cannot be expressed relative to the project root (Path.relative_to raises ValueError). The shared-infra layer only writes inside the project tree, so an absolute destination pointing elsewhere, or a path built from a different root, is rejected before any filesystem write happens.","triggerScenarios":"Calling _write_shared_bytes/_write_shared_text/_ensure_safe_shared_destination (directly or via install_shared_infra/preset writers) with a dest that is absolute and outside project_path, or a relative dest that was joined against another base. Example: _ensure_safe_shared_destination(Path('/repo'), Path('/etc/passwd')).","commonSituations":"Passing a user-supplied config path (e.g. from a config file or CLI arg) that is absolute or anchored to a different checkout; running the CLI from a symlinked or misidentified working directory so project_path and dest disagree; tests using tmp_path fixtures that mix two different temp roots.","solutions":["Ensure dest is constructed by joining project_path: dest = project_path / relative_subpath, never an absolute path from elsewhere","If dest comes from user input, strip a leading '/' or reject absolute paths before calling the API","If the write legitimately targets another tree, call the API with that tree's root as project_path instead","In tests, derive all paths from the same tmp_path instance"],"exampleFix":"// before\n_ensure_safe_shared_destination(Path.cwd(), Path('/opt/data/workflow.yaml'))\n\n// after\n_ensure_safe_shared_destination(Path.cwd(), Path.cwd() / '.specify' / 'workflows' / 'workflow.yaml')","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef is_inside_root(project: Path, dest: Path) -> bool:\n    try:\n        dest.relative_to(project)\n        return True\n    except ValueError:\n        return False\n\n# before calling shared-infra write helpers:\nassert is_inside_root(project_path, dest), f'dest outside project: {dest}'","typeGuard":null,"tryCatchPattern":"try:\n    _ensure_safe_shared_destination(project_path, dest)\nexcept ValueError as e:\n    if 'escapes project root' in str(e):\n        dest = project_path / safe_relative_from(dest)\n    else:\n        raise","preventionTips":["Always build destinations as project_path / relative_parts, never pass absolute paths from config","Reject absolute paths and '..' segments in user-supplied path settings at config-load time","In tests, derive every path from the same tmp_path fixture"],"tags":["filesystem","path-traversal","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}