Skip to content

Symposium & Dial9: Installing a skill silently drops its symlinked files #288

Description

@Fluzko

Part of #294.

Depends on: nothing.

What happens

When a skill's content includes symlinked files, sync installs the skill without them. No warning, exit zero. Anything in the skill that needs those files then fails at runtime with a module error naming a path that does not exist.

dial9 hits this. Three of the five files in its toolkit skill's script directory are symlinks, and those three are the shared trace parser and decoders that every tool imports. So the installed skill arrives with two real files and all five of dial9's analysis tools fail: the two tools that live inside the toolkit survive as files but lose what they import, and the other three reach into the toolkit for those same libraries. dial9's own repository reproduces this today; it goes unnoticed in normal use because the scripts are usually run from the source tree, where the symlinks resolve.

The copier decides what to copy from a directory entry's own type, which for a symlink is neither "file" nor "directory", so symlinks fall through the gap between the two branches and are skipped. A comment records the behaviour as intentional.

Blast radius: anything copied from a live source tree: workspace members, path and git dependencies, [[registry]] path entries, ~/.symposium/plugins/, and git-cloned registries including the builtin recommendations cache. Projects installing from crates.io are unaffected, because packaging already replaces symlinks with real files.

Suggested fix, and why this direction

Dereference. Read the metadata through a call that follows the link rather than from the directory entry. A symlink to a file then reports as a file and the copy already follows it; a symlink to a directory reports as a directory and is walked.

Preserving symlinks instead would not work: the destination is a different tree, so a relative link pointing outside the skill directory dangles there, which is exactly the error users see. Dereferencing also makes source-tree installs agree with crates.io installs, which already deliver real files.

Two guards the fix needs. Metadata lookup fails on a dangling link, and that failure must not propagate, or one broken link fails the whole sync. And a symlinked directory pointing at an ancestor becomes infinite recursion, impossible today precisely because symlinks are skipped, so the cycle guard is born with the fix.

Fix both places. The copier is not the only function with this gap. The scan that collects a directory's contents for comparison has the same one, and that comparison is what makes sync a no-op when nothing changed. Fixing only the copier means the source scan keeps omitting the file while the destination now has it, so every sync sees a difference and does a full delete and recopy, measured as a fresh install on three consecutive runs. Today's second sync on an unchanged source is already a no-op (verified with a source build against the dial9 checkout), so this is a regression the fix must not introduce.

One thing to decide rather than inherit. Dereferencing means a crate could ship a skill file that is a symlink to something outside the crate (a private key, say), and the copy would land in the agent's skill directory, which the agent reads, under an ignore file that hides it. This already applies on the crates.io path, since packaging follows links that escape the package root. Restricting to links whose target stays inside the source root is cheap, and worth deciding explicitly. Whatever is decided has to allow at least the workspace: dial9's toolkit already crosses crate roots. dial9-viewer/ui/decode.js is itself a symlink to ../../dial9-trace-format/js/decode.js, so the toolkit's decode.js is a two-link chain ending in a sibling crate, and dial9-rs/dial9#917 adds a third link. A source-root restriction would drop it.

How to see it

# needs: a dial9 checkout, cargo-agents, and a Claude agent entry in your Symposium
# configuration (otherwise the skills install under a different agent's directory)

cd /path/to/dial9-tokio-telemetry

# the source has five script files, three of them symlinks
ls -l dial9-viewer/skills/dial9-toolkit/scripts/

cargo agents sync

# the installed copy has two
ls .claude/skills/dial9-toolkit/scripts/

# and every tool that needs the missing files
node .claude/skills/dial9-red-flags/scripts/red_flag_scan.js \
     dial9-viewer/ui/public/demo-trace.bin
# -> Error: Cannot find module '<repo>/.claude/ui/trace_parser.js'
#    this fires while loading the script, before the trace is read, so it is not
#    the unrelated crash this same tool has on this trace (dial9's own dial9-rs/dial9-tokio-telemetry#914).

Reproduced with cargo agents sync from a source build, against a consumer project with dial9-viewer as a path dependency and a local [[registry]] entry chaining to it: dial9-toolkit/scripts/ arrives with analyze.js and diagnose_setup.js and none of the three symlinked files.

Done when

  • A skill containing symlinked files installs complete, and what it ships runs from the installed copy.
  • A skill containing a symlinked directory installs that directory's contents.
  • A dangling symlink is skipped with a warning and does not fail the sync.
  • A symlink cycle terminates.
  • Syncing twice with no change to the source leaves the destination untouched, so the fix has not turned every sync into a full recopy.
  • Whether a link pointing outside the source root is followed is a written decision.
  • A test installs a skill whose content includes a symlink and asserts the file arrives with real content.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions