Skip to content

Bug: Custom agent discovery ignores symlinked files in agents directory #914

@JohanVanosmaelAcerta

Description

@JohanVanosmaelAcerta

Bug: Custom agent discovery ignores symlinked files in agents directory

Bug Description

Custom agent files that are symbolic links to .md or .agent.md files are not discovered by the Copilot CLI. The agent discovery logic uses dirent.isFile() which returns false for symbolic links, even when those symlinks point to valid agent definition files.

This is problematic for users who maintain their agent definitions in a central repository and symlink them to ~/.copilot/agents/ for sharing across projects.

Affected Version

@github/copilot 1.0.5 (2025-01-07)
Node.js v22.13.0

Steps to Reproduce

  1. Create a valid agent definition file:

    echo "# Test Agent" > /tmp/test-agent.agent.md
  2. Create a symbolic link in the agents directory:

    ln -s /tmp/test-agent.agent.md ~/.copilot/agents/test-agent.agent.md
  3. Verify the symlink exists and points to a valid file:

    ls -la ~/.copilot/agents/test-agent.agent.md
    # Shows: test-agent.agent.md -> /tmp/test-agent.agent.md
    
    cat ~/.copilot/agents/test-agent.agent.md
    # Shows: # Test Agent
  4. Start Copilot CLI and check available agents - the symlinked agent is not listed.

  5. Replace symlink with a hard link or copy:

    rm ~/.copilot/agents/test-agent.agent.md
    cp /tmp/test-agent.agent.md ~/.copilot/agents/test-agent.agent.md
  6. Restart Copilot CLI - the agent now appears.

Expected Behavior

Symbolic links to valid .md or .agent.md files should be discovered and loaded as custom agents, the same way regular files are.

Root Cause Analysis

The issue is in the WKn function (agent discovery) in the bundled index.js. The relevant code pattern:

// Current implementation (simplified/prettified from minified source)
async function discoverAgents(agentsDir) {
  let entries = await readdir(agentsDir, { withFileTypes: true });

  for (let entry of entries) {
    // BUG: isFile() returns false for symbolic links
    if (entry.isFile() && entry.name.endsWith(".md")) {
      // Process agent...
    }
  }
}

When using readdir with { withFileTypes: true }, the returned Dirent objects have type information. For symbolic links:

  • dirent.isFile() returns false
  • dirent.isSymbolicLink() returns true

This means symlinks are silently skipped even when they point to valid files.

Note: Interestingly, if the entire ~/.copilot/agents directory itself is a symlink to another directory, it works correctly because readdir(path) resolves path-level symlinks automatically. Only symlinked files within the directory are affected.

Suggested Fix

Option 1: Check for symlinks and follow them (Recommended)

async function discoverAgents(agentsDir) {
  let entries = await readdir(agentsDir, { withFileTypes: true });

  for (let entry of entries) {
    if (!entry.name.endsWith(".md")) continue;

    // Handle both regular files and symlinks to files
    let isValidFile = entry.isFile();

    if (entry.isSymbolicLink()) {
      try {
        const resolvedPath = path.join(agentsDir, entry.name);
        const stat = await fs.stat(resolvedPath); // stat() follows symlinks
        isValidFile = stat.isFile();
      } catch {
        // Broken symlink, skip it
        continue;
      }
    }

    if (isValidFile) {
      // Process agent...
    }
  }
}

Option 2: Use fs.stat() instead of dirent types

async function discoverAgents(agentsDir) {
  let entries = await readdir(agentsDir); // Without withFileTypes

  for (let name of entries) {
    if (!name.endsWith(".md")) continue;

    const filePath = path.join(agentsDir, name);
    try {
      const stat = await fs.stat(filePath); // Automatically follows symlinks
      if (stat.isFile()) {
        // Process agent...
      }
    } catch {
      continue;
    }
  }
}

Option 3: Minimal change - also accept symlinks

// Change this:
if (entry.isFile() && entry.name.endsWith(".md"))

// To this:
if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md"))

Note: Option 3 is the simplest but doesn't verify that symlinks point to actual files (could accept symlinks to directories or broken symlinks).

Additional Context

  • OS: Linux (WSL2) 6.6.87.2-microsoft-standard-WSL2
  • Architecture: x86_64
  • Shell: bash
  • Terminal: Windows Terminal

Use Case

We maintain a shared repository of custom agent definitions that multiple team members use. The intended workflow is:

  1. Clone the shared agents repository
  2. Symlink individual agents to ~/.copilot/agents/
  3. Pull updates to get new/updated agents automatically

Currently, we must use hard links (which break on cross-filesystem setups and after git operations) or manually copy files (which don't auto-update).

Workarounds

  1. Hard links instead of symbolic links (same filesystem only, may break after git pulls)
  2. Copy files instead of symlinking (no auto-updates)
  3. Symlink the entire directory - make ~/.copilot/agents itself a symlink to the source directory (works, but less flexible)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions