github/spec-kit · error · Error

errors.join('; ')

Error message

errors.join('; ')

What it means

Second guard inside _shared_relative_path(): the path did relativize, but the result is absolute or contains a '..' component. This catches relative destinations like '../outside/file' that Path.relative_to can still produce when dest was built as project_path/'..'/'elsewhere', meaning the write would climb out of the project root.

Source

Thrown at src/specify_cli/events.py:1903

                    checks = " || ".join(
                        f"input.tool === {json.dumps(t.lower())}" for t in tools
                    )
                    body_lines.append(
                        f"    try {{ if ({checks}) {{ runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); }} }} catch (e) {{ errors.push((e as Error).message); }}"
                    )
                else:
                    body_lines.append(
                        f"    try {{ runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); }} catch (e) {{ errors.push((e as Error).message); }}"
                    )
            elif is_injection:
                body_lines.append(
                    f"    try {{ const ctx = runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); if (ctx) contexts.push(ctx); }} catch (e) {{ errors.push((e as Error).message); }}"
                )
            else:
                body_lines.append(
                    f"    try {{ runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); }} catch (e) {{ errors.push((e as Error).message); }}"
                )
        body_lines.append("    if (errors.length > 0) { throw new Error(errors.join('; ')); }")

        if native.startswith("tool.execute."):
            ts_hook = native
            event_entries.append(
                f"function _{ev}(input: any, output: any) {{\n"
                + "\n".join(body_lines) + "\n"
                "  }"
            )
            plugin_returns.append(
                f"    {json.dumps(ts_hook)}: async (input: any, output: any) => {{\n"
                f"      _{ev}(input, output);\n"
                f"    }},"
            )
        elif native == "experimental.chat.system.transform":
            body_lines.append('    return contexts.join("\\n\\n");')
            event_entries.append(
                f"function _{ev}(input: any, output: any): string {{\n"
                + "\n".join(body_lines) + "\n"

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Normalize and validate the relative portion: reject any dest whose parts contain '..' before calling the API
  2. Build destinations from trusted constants plus sanitized user input (allow only [A-Za-z0-9._-] segments)
  3. Fix the configuration value that introduced the '..' segment
  4. Run Path(os.path.normpath(dest)) checks in your own layer and fail early with a clearer message

Example fix

# before
dest = project / cfg.shared_dir / 'file.yaml'  # cfg.shared_dir = '../outside'

# after
if '..' in Path(cfg.shared_dir).parts:
    raise ValueError('shared_dir must stay inside the project')
dest = project / cfg.shared_dir / 'file.yaml'
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path, PurePosixPath

def safe_relative(user_path: str) -> Path | None:
    p = PurePosixPath(user_path)
    if p.is_absolute() or '..' in p.parts:
        return None
    return Path(p)

rel = safe_relative(cfg.shared_dir)
if rel is None:
    raise ValueError(f'unsafe shared dir: {cfg.shared_dir!r}')

Try / catch

try:
    rel = _shared_relative_path(project_path, dest)
except ValueError as e:
    if 'escapes project root' in str(e):
        raise SystemExit(f'configuration points outside the project: {e}') from e
    raise

Prevention

When it happens

Trigger: Calling shared-infra write helpers with dest such as project_path / '..' / 'sibling' / 'file', or a relative dest containing '..' segments that survives relative_to. E.g. _write_shared_bytes(project, project/'.specify'/'..'/'..'/'victim', b'x').

Common situations: User-configured subdir containing '..' (e.g. config value "../shared"); sanitizing paths by string concatenation instead of resolve(); templates or presets whose directory fields were edited to traverse upward.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/f0edb29ceb1eef1f. Report an issue: GitHub.