Skip to content

Namespace copyPackFile destinations with composed pack ID to prevent cross-pack collisions #278

@bguidolim

Description

@bguidolim

Problem

When two packs define a copyPackFile component with the same destination filename and the same CopyFileType, they collide silently:

  1. File level: The second pack's file overwrites the first at the shared path (e.g., ~/.claude/hooks/lint.sh). Last writer wins with no warning.
  2. Settings level: For hooks, addHookEntry deduplicates by command string (bash ~/.claude/hooks/lint.sh), so only one settings entry exists. The second pack's addHookEntry call returns false.
  3. Artifact ownership: Both packs record the same path/command in their PackArtifactRecord. Removing Pack A strips the hook from settings even though Pack B still needs it. The next sync re-adds it, but there's an inconsistency window.

This affects all CopyFileType cases: hooks, skills, commands, agents, and generic files — any two packs using the same destination filename under the same subdirectory will collide.

Proposed Solution

Use the composed component ID (which already includes the pack prefix, e.g., my-pack.lint-hook) to derive the destination filename, preventing collisions by construction.

For example, a component my-pack.lint-hook with destination: lint.sh would be installed as:

  • Current: ~/.claude/hooks/lint.sh
  • Proposed: ~/.claude/hooks/my-pack.lint-hook.sh (or similar namespaced scheme)

The hook command in settings would change accordingly:

  • Current: bash ~/.claude/hooks/lint.sh
  • Proposed: bash ~/.claude/hooks/my-pack.lint-hook.sh

Components to audit

All CopyFileType destinations share a flat namespace per subdirectory:

  • .hook.claude/hooks/
  • .skill.claude/skills/
  • .command.claude/commands/
  • .agent.claude/agents/
  • .generic.claude/

Each needs the same namespacing treatment.

Migration Plan

Existing installations have files at the old (non-namespaced) paths. A migration is required:

  1. Detection: On first sync after the change, compare old artifact records (non-namespaced paths) against the new naming scheme.
  2. Rename: Move existing files from old paths to new namespaced paths.
  3. Settings update: For hooks, remove the old command string from settings and add the new one.
  4. Artifact record update: Update PackArtifactRecord entries (files, fileHashes, hookCommands) to reflect new paths.
  5. State version bump: Increment the ProjectState version to trigger migration on next sync.
  6. Backward compatibility: Consider a transitional period where both old and new paths are recognized during doctor checks, to avoid false positives for users who haven't re-synced yet.

References

  • Sources/mcs/TechPack/Component.swiftCopyFileType enum
  • Sources/mcs/Install/ConfiguratorSupport.swift — hook command construction (hookCommandPrefix + destination)
  • Sources/mcs/Install/GlobalSyncStrategy.swift — hook stripping by prefix match
  • Sources/mcs/Install/Configurator.swift — artifact tracking for hook commands

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium prioritybugSomething isn't workingtech-debtTechnical debt and code quality improvements

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions