microsoft/aspire · error · InvalidOperationException

Aspire skills bundle path

Error message

Aspire skills bundle path '{0}' must be relative.

What it means

NormalizeRelativePath rejects bundle paths that are rooted (absolute), such as '/etc/passwd' or 'C:\Windows\...'. Bundle contents must be addressed relative to the bundle root so extraction/copying can never target arbitrary filesystem locations. Aspire throws this InvalidOperationException when a rooted path is encountered.

Solutions

  1. Change the path to be relative to the bundle root, e.g. "skills/my-skill/SKILL.md"
  2. Strip any leading slash, drive letter, or UNC prefix from manifest entries
  3. Repackage the bundle with a tool that emits relative paths

Example fix

// before (skill-manifest.json)
{ "path": "/home/user/app/skills/my-skill/SKILL.md" }
// after
{ "path": "skills/my-skill/SKILL.md" }
Defensive patterns

Strategy: validation

Validate before calling

if (Path.IsPathRooted(entry.Path) || Path.IsPathFullyQualified(entry.Path))
    throw new Exception($"Path '{entry.Path}' must be relative to the bundle root.");

Try / catch

try { await provider.CreateAsync(...); }
catch (InvalidOperationException ex) when (ex.Message.Contains("must be relative")) { /* rewrite manifest paths as relative */ }

Prevention

When it happens

Trigger: A manifest entry or bundle path in skill-manifest.json is an absolute path (leading '/', drive letter, or UNC path) and is passed through AspireSkillsBundleProvider.CreateAsync.

Common situations: Authoring a manifest on Windows with 'C:\...' paths; copy-pasting absolute file paths from an editor; a packaging tool emitting absolute paths.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/495255ce509931de. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundleProvider.cs:414

        }

        return null;
    }

    internal static string NormalizeRelativePath(string? relativePath)
    {
        if (string.IsNullOrWhiteSpace(relativePath))
        {
            throw new InvalidOperationException("Aspire skills bundle contains an empty relative path.");
        }

        var normalizedPath = relativePath
            .Replace('/', Path.DirectorySeparatorChar)
            .Replace('\\', Path.DirectorySeparatorChar);

        if (Path.IsPathRooted(normalizedPath))
        {
            throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Aspire skills bundle path '{0}' must be relative.", relativePath));
        }

        var segments = normalizedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length == 0 || segments.Any(static segment => !IsPortablePathSegment(segment)))
        {
            throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Aspire skills bundle path '{0}' is not safe.", relativePath));
        }

        return Path.Combine(segments);
    }

    private static bool IsPortablePathSegment(string segment)
    {
        // Bundle paths can be validated on one platform and installed on another. Reject the
        // Windows-invalid character set everywhere so ':' cannot create an NTFS alternate data
        // stream and other invalid filenames cannot enter a cached bundle.
        // See https://learn.microsoft.com/windows/win32/fileio/naming-a-file.
        return segment is not "." and not ".." &&

View on GitHub (pinned to 25830f84bd)