Skip to content

gh skill update --dir relocates the skill and deletes the original install directory #13370

@FunJim

Description

@FunJim

Describe the bug

gh skill update --dir <dir> moves an updated skill to a directory different from where it was originally installed, and deletes the original install directory. This causes silent data loss for any agent configuration, symlinks, or tooling that referenced the original path, and silently breaks discovery for agents that were pointed at the original install root.

The issue happens when a skill is installed under a namespaced path (the install command's --dir ended at a "namespace" segment, e.g. .agents/skills/anthropics-skills), and then gh skill update is later invoked with a --dir one level higher (e.g. .agents/skills). The update scanner classifies the existing layout as "namespaced", and the update path then re-resolves the install root by walking two filepath.Dir levels up instead of one, ending up at the wrong base directory. The newly-installed copy lands one directory level too shallow, and the original directory is then os.RemoveAll-ed by the "migration cleanup" code in pkg/cmd/skills/update/update.go.

Affected version

gh version 2.92.0 (2026-04-28)
https://github.com/cli/cli/releases/tag/v2.92.0

Steps to reproduce the behavior

In an empty project directory:

# 1. Install the claude-api skill into a custom, deeper directory.
gh skill install \
    --scope project \
    --agent codex \
    anthropics/skills claude-api \
    --dir .agents/skills/anthropics-skills

# Resulting layout (correct):
#   .agents/skills/anthropics-skills/claude-api/SKILL.md
# 2. Run an update from one directory level above the install root.
gh skill update --dir ./.agents/skills

Output:

1 update(s) available:
  • anthropics-skills/claude-api (anthropics/skills) 284049f3 > add30405 [main]

? Update 1 skill(s)? Yes
✓ Updated anthropics-skills/claude-api

Expected vs actual behavior

Expected:

  • The updated skill files should overwrite the original install location: .agents/skills/anthropics-skills/claude-api/.
  • The original directory must not be deleted.
  • More generally, gh skill update should be a content-only update at the location where the skill currently lives. It should never relocate a skill, and certainly never delete the previous install path without an explicit user opt-in.

Actual:

  • The updated skill is installed at .agents/skills/claude-api/ (one directory level too shallow).
  • The original directory .agents/skills/anthropics-skills/claude-api/ is removed via os.RemoveAll. If the now-empty namespace parent (.agents/skills/anthropics-skills/) was not used by anything else, it is removed too.
  • Any agent config, lockfile reference, IDE workspace, symlink, or VCS-tracked tooling pointing at the original path silently breaks.

Root cause (from the source)

In pkg/cmd/skills/update/update.go, when the update path runs with a --dir flag (so u.local.host == nil), the install target is computed by walking up from the previously-detected skill directory:

// pkg/cmd/skills/update/update.go (around L400-L410)
if u.local.host == nil {
    base := filepath.Dir(u.local.dir)
    if strings.Contains(u.local.name, "/") {
        base = filepath.Dir(base)
    }
    installOpts.Dir = base
}

u.local.name here is the display / install-name produced by the scanner. For a flat {dir}/{name}/SKILL.md layout the scanner returns just claude-api, but for a {dir}/{namespace}/{name}/SKILL.md layout it joins them into anthropics-skills/claude-api:

// pkg/cmd/skills/update/update.go (around L516)
installName := e.Name() + "/" + sub.Name()

So when the user passes --dir .agents/skills to update, the scanner sees .agents/skills/anthropics-skills/claude-api/SKILL.md, classifies it as namespaced, and the update branch walks two filepath.Dir levels up — landing on .agents/skills instead of the correct .agents/skills/anthropics-skills. The installer then writes flat ({Dir}/{skill.Name}), producing .agents/skills/claude-api.

The "migration" cleanup block immediately after compares the new and old paths and, because they differ, deletes the old one:

// pkg/cmd/skills/update/update.go (around L421-L433)
newDir := filepath.Join(installOpts.Dir, u.skill.Name)
...
if newDir != "" && u.local.dir != "" && filepath.Clean(newDir) != filepath.Clean(u.local.dir) {
    _ = os.RemoveAll(u.local.dir)
    parent := filepath.Dir(u.local.dir)
    if entries, readErr := os.ReadDir(parent); readErr == nil && len(entries) == 0 {
        _ = os.Remove(parent)
    }
}

Two underlying problems:

  1. Layout misclassification. The scanner cannot tell apart a true namespaced layout ({dir}/{namespace}/{name}) from a flat install whose --dir happened to point one level deeper than the user's update --dir ({outerDir}/{customSubdir}/{name}). It then encodes that guess into installName and feeds it to the relocation logic.
  2. Implicit relocation + deletion on update. Even when the layout has genuinely changed, silently os.RemoveAll-ing the previous install path on a routine gh skill update is surprising. At minimum this should be opt-in (or a separate gh skill migrate action), and the new install location should match the location the skill was discovered at.

Suggested fix

When updating a skill that was found via --dir, the install target should be filepath.Dir(u.local.dir) regardless of whether the install-name contains a / — i.e. write the new copy back into the exact directory the existing SKILL.md lives in, rather than re-deriving a base from a display name. Equivalently, prefer u.local.dir (the on-disk parent of the SKILL.md) as ground truth instead of the parsed installName.

The "migration cleanup" branch should also not run for the --dir (no host) path, or should be gated on an explicit flag.

Logs

❯ gh skill update --dir ./.agents/skills

1 update(s) available:
  • anthropics-skills/claude-api (anthropics/skills) 284049f3 > add30405 [main]

? Update 1 skill(s)? Yes
✓ Updated anthropics-skills/claude-api

❯ gh --version
gh version 2.92.0 (2026-04-28)
https://github.com/cli/cli/releases/tag/v2.92.0

After the update:

.agents/skills/claude-api/SKILL.md                     # new (unexpected location)
.agents/skills/anthropics-skills/                      # removed (or empty + removed)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggh-skillrelating to the gh skill commandpriority-2Affects more than a few users but doesn't prevent core functions

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions