github/spec-kit · error · Error
specify event ${{command}} (${{event}}) failed: ${{(e as Err
Error message
specify event ${{command}} (${{event}}) failed: ${{(e as Error).message}} What it means
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.
Source
Thrown at src/specify_cli/events.py:443
try {{
// execFileSync with an argv array invokes the interpreter directly — no
// shell — so command/event strings with metacharacters can't break out
// of the dispatcher argument (C9). The dispatcher arg is seconds; the
// execFileSync timeout is ms with a buffer so the outer cap fires after
// the dispatcher's inner subprocess (S3). stdout is captured and
// returned so context-injection hooks (experimental.chat.system.transform,
// chat.message) can push it into their outputs; stderr stays inherited so
// dispatcher errors remain visible (C11).
return execFileSync(INTERPRETER, [DISPATCHER, command, event, String(timeoutSec)], {{
input: JSON.stringify({{ input, output }}),
stdio: ['pipe', 'pipe', 'inherit'],
encoding: 'utf-8',
timeout: (timeoutSec + {buffer}) * 1000,
}});
}} catch (e) {{
// Propagate to OpenCode's hook machinery so only this hook is rejected,
// not the entire host process. process.exit() would kill the agent.
throw new Error(`specify event ${{command}} (${{event}}) failed: ${{(e as Error).message}}`);
}}
}}
// Cache session_start handler output per sessionID so non-idempotent
// handlers (setup, telemetry, file-mutating scripts) run once per session
// instead of on every LLM request (experimental.chat.system.transform
// fires per LLM turn). Evicted on session.deleted.
const sessionStartCache = new Map<string, string>();
{event_entries}
export default (async ({{ client, project, directory, $ }}) => {{
resolveDispatcher(directory);
return {{
{plugin_returns}
}};
}});
'''View on GitHub (pinned to bf88c9f9a8)
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
Example fix
// before
_ensure_safe_shared_destination(Path.cwd(), Path('/opt/data/workflow.yaml'))
// after
_ensure_safe_shared_destination(Path.cwd(), Path.cwd() / '.specify' / 'workflows' / 'workflow.yaml') Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def is_inside_root(project: Path, dest: Path) -> bool:
try:
dest.relative_to(project)
return True
except ValueError:
return False
# before calling shared-infra write helpers:
assert is_inside_root(project_path, dest), f'dest outside project: {dest}' Try / catch
try:
_ensure_safe_shared_destination(project_path, dest)
except ValueError as e:
if 'escapes project root' in str(e):
dest = project_path / safe_relative_from(dest)
else:
raise Prevention
- 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
When it happens
Trigger: 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')).
Common situations: 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.
Related errors
- errors.join('; ')
- Error: --number requires a value
- ERROR: Unknown option '{arg}'. Use --help for usage informat
- Output path {candidate!r} escapes directory {base!r}
- Invalid command name {cmd_name!r}: {name_reason}
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/9a85b56308d64f91.
Report an issue: GitHub.