Skip to content

toolcraft-openapi: allow generated skill output through symlinks that resolve inside project #435

Description

@kamilio

Summary

toolcraft-openapi@0.0.71 rejects generated skill output paths when any component is a symlink, even if the symlink resolves to a path inside the project directory.

This blocks poe-agent-tools from running npm start because that repo has:

.claude/skills -> ../.agents/skills

The symlink is intentionally internal to the repo, but generation fails with:

Error: Generated skill output must remain inside the project directory.
    at assertSafeProjectPath (.../node_modules/toolcraft-openapi/dist/bin/generate.js:562:23)
    at async restoreGeneratedSkillFiles (.../node_modules/toolcraft-openapi/dist/bin/generate.js:624:9)
    at async syncGeneratedClient (.../node_modules/toolcraft-openapi/dist/bin/generate.js:159:13)

Repro

From poe-agent-tools:

npm start

The script does:

npm install --force toolcraft@latest toolcraft-openapi@latest
npm run generate

Generation then writes generated skill docs to the hard-coded path:

.claude/skills/poe-agent-tools/SKILL.md

But .claude/skills is a symlink to ../.agents/skills, so the safety check rejects it.

Current behavior

assertSafeProjectPath rejects if any path component is a symbolic link.

That prevents unsafe symlink escapes, but it also rejects safe project-internal links like:

<repo>/.claude/skills -> <repo>/.agents/skills

The failed run can also leave partial generated output behind because rollback hits the same symlink check.

Expected behavior

Generated skill output should be allowed when the resolved/canonical target remains inside the project directory.

A symlink should only be rejected if resolving it escapes the project root, e.g. into another repo, the user home directory, /tmp, etc.

Conceptually:

const root = await fs.realpath(cwd);
const targetParent = await fs.realpath(path.dirname(filePath));
const relative = path.relative(root, targetParent);

if (
  relative === ".." ||
  relative.startsWith(`..${path.sep}`) ||
  path.isAbsolute(relative)
) {
  throw new Error("Generated skill output must remain inside the project directory.");
}

This would preserve the protection against path escapes while allowing repo-internal symlink layouts.

Additional context

toolcraft-openapi-generate --inspect --output-format markdown reports the upstream OpenAPI document is fully compatible:

100% compatible · 35 operations · 35 supported · 0 unsupported

So the issue is not OpenAPI compatibility. It is the generated skill-file path safety check interacting with a repo-internal symlink.

A related improvement would be to make the generated skill output path configurable instead of hard-coding .claude/skills/<name>/SKILL.md, but relaxing the symlink check for canonical in-project targets is the direct fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is not workingreleased

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions