Skip to content

Instantly share code, notes, and snippets.

@cjsut
Last active April 17, 2026 20:52
Show Gist options
  • Select an option

  • Save cjsut/3dfd42654911fa91ea17001f613560f6 to your computer and use it in GitHub Desktop.

Select an option

Save cjsut/3dfd42654911fa91ea17001f613560f6 to your computer and use it in GitHub Desktop.
Copilot customization setup — unified VS Code + CLI config via symlinks

Copilot Customizations

Personal customizations for GitHub Copilot, served to both VS Code and Copilot CLI via local symlinks (or NTFS junctions on Windows). One source of truth, two clients with different discovery models.

Contents

copilot-config/
    instructions/       ← .instructions.md files (rules and guidelines)
    agents/             ← .agent.md files (specialized personas)
    skills/             ← SKILL.md directories (reusable capabilities)
    setup.ps1           ← Windows setup (junctions, env var, VS Code settings)
    setup.sh            ← macOS/Linux setup (symlinks, env var, VS Code settings)
    README.md

How discovery works

VS Code and Copilot CLI look for customizations in different places. This setup bridges both from a single source directory using symlinks (macOS/Linux) or NTFS junctions (Windows).

VS Code — user-level defaults

VS Code discovers user-level customizations from ~/.copilot/ subfolders automatically:

Type Default user path How this setup satisfies it
Instructions ~/.copilot/instructions/ Symlink/junction → source
Agents ~/.copilot/agents/ Symlink/junction → source
Skills (no built-in user default) chat.skillsLocations setting points to source

No chat.instructionsFilesLocations or chat.agentFilesLocations settings are needed — junctions place files exactly where VS Code already looks by default.

See Customize AI in VS Code · Parent repository discovery

Copilot CLI — COPILOT_CUSTOM_INSTRUCTIONS_DIRS

The CLI reads a comma-separated list of directories from the COPILOT_CUSTOM_INSTRUCTIONS_DIRS environment variable and treats each one as a virtual repository root, applying the standard .github/ convention:

Type Path the CLI looks for (within each dir) How this setup satisfies it
Instructions .github/instructions/**/*.instructions.md Symlink/junction → source
Skills .github/skills/*/SKILL.md Symlink/junction → source
Agent instrs AGENTS.md at root (not used — see note below)
Repo-wide .github/copilot-instructions.md (not used — per-repo only)

See Adding custom instructions for Copilot CLI · Using custom agents in CLI

Why .github/ inside ~/.copilot/?

The CLI reuses the repository convention (repo-root/.github/instructions/) for personal config directories. When COPILOT_CUSTOM_INSTRUCTIONS_DIRS points to ~/.copilot/, the CLI looks for ~/.copilot/.github/instructions/. This is an emergent artifact — not a purpose-built path — which is why the setup creates a .github/ directory inside ~/.copilot/ with its own links back to the same source.

Setup

Run setup.ps1 (Window) or setup.sh (macOS/Linux) to create the necessary symlinks/junctions, set the environment variable, and configure VS Code settings.

These scripts are idempotent and safe to re-run.

Windows (PowerShell):

& "path\to\copilot-config\setup.ps1"

macOS / Linux (bash):

./path/to/copilot-config/setup.sh

What it creates

~/.copilot/
    instructions/              → (source)/instructions/    ← VS Code user-level discovery
    agents/                    → (source)/agents/          ← VS Code user-level discovery
    .github/
        instructions/          → (source)/instructions/    ← CLI (virtual repo root convention)
        skills/                → (source)/skills/          ← CLI (virtual repo root convention)

The same source files appear at two different relative paths because each client expects them in a different location. The links are duplicated by design, not redundant.

Environment variable

COPILOT_CUSTOM_INSTRUCTIONS_DIRS = ~/.copilot

This is the linchpin for CLI discovery. The CLI scans .github/instructions/ and .github/skills/ within each directory listed here. Without it, the CLI only discovers customizations inside the current working directory's repo.

On Windows, set as a persistent User environment variable. On macOS/Linux, exported in ~/.zshrc or ~/.bashrc.

VS Code settings (automated by setup scripts)

// Skills need an explicit setting because there is no built-in user default
// path like there is for instructions and agents.
"chat.skillsLocations": {
    "~/.copilot/skills": true,                              // tool-installed skills
    "~/path/to/copilot-config/skills": true,                // handcrafted skills (your source dir)
},

// Walk up from workspace folder to the nearest .git root and discover
// customizations from all folders in between. Useful for monorepos where
// you open a subfolder rather than the repo root.
"chat.useCustomizationsInParentRepositories": true,

// Search subfolders for AGENTS.md files. Each subfolder's AGENTS.md applies
// to work in that subtree, enabling per-module instructions in large repos.
"chat.useNestedAgentsMdFiles": true,

Customization types

Instructions

Rules and guidelines that influence how AI generates code. Defined in *.instructions.md files with optional YAML frontmatter.

  • applyTo: "**" — always-on, applied to every chat request (e.g. coding philosophy)
  • applyTo: "**/*.ts,**/*.tsx" — applied when working on matching files (e.g. TypeScript conventions)
  • description: "Use when..." — on-demand, loaded when the AI determines the task matches the description

See VS Code docs · GitHub docs

Agents

Specialized personas with their own instructions, tool restrictions, and model preferences. Defined in *.agent.md files. Activated by selecting the agent from the chat dropdown.

See VS Code docs

Skills

Reusable capabilities bundled as a directory with a SKILL.md file plus optional scripts, templates, and examples. Available as /slash-commands in chat or auto-detected by the AI based on the skill's description. Skills are portable across VS Code, Copilot CLI, and Copilot cloud agent via the Agent Skills standard.

Tool-installed skills go in ~/.copilot/skills/ (local, not synced). Only handcrafted skills belong in this repo.

See VS Code docs · GitHub docs

Verification

To confirm customizations are being loaded:

  • VS Code: Right-click in the Chat view → Diagnostics to see all loaded instruction files, agents, and skills
  • VS Code: Type /instructions in chat to open the Configure Instructions menu and see active files
  • VS Code: Check the References section at the top of any chat response to see which instructions were used
  • Copilot CLI: Run copilot then /context to see loaded instructions and token usage
<#
.SYNOPSIS
Sets up local junctions and environment variables so VS Code and Copilot CLI
discover customizations from this directory.
.DESCRIPTION
This script is idempotent — safe to re-run after an OS reinstall or if
junctions were accidentally deleted. It will skip any junction that already
exists and points to the correct target.
Source of truth: wherever this script lives (e.g. a OneDrive-synced folder)
Local plumbing: ~/.copilot/ (junctions + .github structure)
.NOTES
Run from any directory. No admin required (NTFS junctions don't need elevation).
#>
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Source = $PSScriptRoot
$Target = Join-Path $HOME '.copilot'
# --- Helpers ---
function Set-Junction {
param(
[string]$Path,
[string]$Destination
)
if (Test-Path $Path) {
$item = Get-Item $Path -Force
if ($item.LinkTarget -eq $Destination) {
Write-Host " OK $Path -> $Destination" -ForegroundColor DarkGray
return
}
if ($item.LinkTarget) {
Write-Host " FIX $Path (was -> $($item.LinkTarget))" -ForegroundColor Yellow
$item.Delete()
} else {
Write-Warning "$Path exists and is not a junction. Skipping."
return
}
}
New-Item -ItemType Junction -Path $Path -Target $Destination | Out-Null
Write-Host " NEW $Path -> $Destination" -ForegroundColor Green
}
# --- Validate source ---
if (-not (Test-Path $Source)) {
Write-Error "Source not found: $Source"
}
# --- Create ~/.copilot if needed ---
New-Item -ItemType Directory -Path $Target -Force | Out-Null
# --- VS Code junctions (default discovery paths) ---
Write-Host "`nVS Code junctions:" -ForegroundColor Cyan
Set-Junction "$Target\instructions" "$Source\instructions"
Set-Junction "$Target\agents" "$Source\agents"
# --- CLI junctions (.github structure) ---
Write-Host "`nCopilot CLI junctions (.github):" -ForegroundColor Cyan
New-Item -ItemType Directory -Path "$Target\.github" -Force | Out-Null
Set-Junction "$Target\.github\instructions" "$Source\instructions"
Set-Junction "$Target\.github\skills" "$Source\skills"
# --- Environment variable ---
Write-Host "`nEnvironment variable:" -ForegroundColor Cyan
$envName = 'COPILOT_CUSTOM_INSTRUCTIONS_DIRS'
$current = [System.Environment]::GetEnvironmentVariable($envName, 'User')
if ($current -eq $Target) {
Write-Host " OK $envName = $Target" -ForegroundColor DarkGray
} else {
[System.Environment]::SetEnvironmentVariable($envName, $Target, 'User')
Write-Host " SET $envName = $Target" -ForegroundColor Green
}
# --- VS Code settings ---
Write-Host "`nVS Code settings:" -ForegroundColor Cyan
$settingsPath = Join-Path $env:APPDATA 'Code\User\settings.json'
if (Test-Path $settingsPath) {
$raw = Get-Content $settingsPath -Raw
# Strip comments (// style) and trailing commas for parsing
$cleaned = $raw -replace '//.*$', '' -replace ',(\s*[}\]])', '$1'
$settings = $cleaned | ConvertFrom-Json
$dirty = $false
# chat.skillsLocations
# Use ~ paths with forward slashes for portability in VS Code settings.
$sourceRelative = $Source.Replace($HOME, '~').Replace('\', '/')
$expectedSkills = @{
'~/.copilot/skills' = $true
"$sourceRelative/skills" = $true
}
$currentSkills = $settings.'chat.skillsLocations'
$needsSkills = -not $currentSkills -or
$currentSkills."$sourceRelative/skills" -ne $true
if ($needsSkills) {
$settings | Add-Member -NotePropertyName 'chat.skillsLocations' -NotePropertyValue $expectedSkills -Force
Write-Host " SET chat.skillsLocations" -ForegroundColor Green
$dirty = $true
} else {
Write-Host " OK chat.skillsLocations" -ForegroundColor DarkGray
}
# chat.useCustomizationsInParentRepositories
if ($settings.'chat.useCustomizationsInParentRepositories' -eq $true) {
Write-Host " OK chat.useCustomizationsInParentRepositories = true" -ForegroundColor DarkGray
} else {
$settings | Add-Member -NotePropertyName 'chat.useCustomizationsInParentRepositories' -NotePropertyValue $true -Force
Write-Host " SET chat.useCustomizationsInParentRepositories = true" -ForegroundColor Green
$dirty = $true
}
# chat.useNestedAgentsMdFiles (experimental)
if ($settings.'chat.useNestedAgentsMdFiles' -eq $true) {
Write-Host " OK chat.useNestedAgentsMdFiles = true" -ForegroundColor DarkGray
} else {
$settings | Add-Member -NotePropertyName 'chat.useNestedAgentsMdFiles' -NotePropertyValue $true -Force
Write-Host " SET chat.useNestedAgentsMdFiles = true" -ForegroundColor Green
$dirty = $true
}
if ($dirty) {
$settings | ConvertTo-Json -Depth 10 | Set-Content $settingsPath -Encoding utf8NoBOM
Write-Host " Wrote $settingsPath" -ForegroundColor Green
}
} else {
Write-Warning "VS Code settings not found at $settingsPath — set manually."
}
Write-Host "`nDone." -ForegroundColor Green
#!/usr/bin/env bash
#
# Sets up local symlinks and environment variables so VS Code and Copilot CLI
# discover customizations from this directory.
#
# Idempotent — safe to re-run after an OS reinstall or if symlinks break.
# Source of truth: wherever this script lives (e.g. a cloud-synced folder).
# Local plumbing: ~/.copilot/ (symlinks + .github structure)
set -euo pipefail
# --- Resolve source from script location ---
SOURCE="$(cd "$(dirname "$0")" && pwd)"
TARGET="$HOME/.copilot"
# --- Helpers ---
set_symlink() {
local path="$1"
local dest="$2"
if [ -L "$path" ]; then
local current
current="$(readlink "$path")"
if [ "$current" = "$dest" ]; then
printf ' OK %s -> %s\n' "$path" "$dest"
return
fi
printf ' FIX %s (was -> %s)\n' "$path" "$current"
elif [ -e "$path" ]; then
printf ' WARN %s exists and is not a symlink. Skipping.\n' "$path"
return
fi
ln -sfn "$dest" "$path"
printf ' NEW %s -> %s\n' "$path" "$dest"
}
# --- Validate source ---
if [ ! -d "$SOURCE" ]; then
printf 'Source not found: %s\n' "$SOURCE" >&2
exit 1
fi
# --- Create ~/.copilot if needed ---
mkdir -p "$TARGET"
# --- VS Code symlinks (default discovery paths) ---
printf '\nVS Code symlinks:\n'
set_symlink "$TARGET/instructions" "$SOURCE/instructions"
set_symlink "$TARGET/agents" "$SOURCE/agents"
# --- CLI symlinks (.github structure) ---
printf '\nCopilot CLI symlinks (.github):\n'
mkdir -p "$TARGET/.github"
set_symlink "$TARGET/.github/instructions" "$SOURCE/instructions"
set_symlink "$TARGET/.github/skills" "$SOURCE/skills"
# --- Environment variable ---
printf '\nEnvironment variable:\n'
ENV_NAME='COPILOT_CUSTOM_INSTRUCTIONS_DIRS'
ENV_LINE="export ${ENV_NAME}=\"${TARGET}\""
MARKER="# copilot-config setup"
# Detect shell rc file
case "${SHELL:-/bin/bash}" in
*/zsh) RC_FILE="$HOME/.zshrc" ;;
*) RC_FILE="$HOME/.bashrc" ;;
esac
if grep -qF "$MARKER" "$RC_FILE" 2>/dev/null; then
printf ' OK %s (already in %s)\n' "$ENV_NAME" "$RC_FILE"
else
printf '\n%s\n%s\n' "$MARKER" "$ENV_LINE" >> "$RC_FILE"
printf ' SET %s in %s\n' "$ENV_NAME" "$RC_FILE"
fi
# Also export for the current session
export "$ENV_NAME"="$TARGET"
# --- VS Code settings ---
printf '\nVS Code settings:\n'
# Determine settings.json path
if [ "$(uname)" = "Darwin" ]; then
VSCODE_SETTINGS="$HOME/Library/Application Support/Code/User/settings.json"
else
VSCODE_SETTINGS="${XDG_CONFIG_HOME:-$HOME/.config}/Code/User/settings.json"
fi
# Use ~ for portability in VS Code settings
SOURCE_TILDE="${SOURCE/#$HOME/~}"
if command -v python3 &>/dev/null && [ -f "$VSCODE_SETTINGS" ]; then
python3 - "$VSCODE_SETTINGS" "$SOURCE_TILDE" <<'PYTHON_SCRIPT'
import json
import re
import sys
settings_path = sys.argv[1]
source_tilde = sys.argv[2]
with open(settings_path, "r", encoding="utf-8") as f:
raw = f.read()
# Strip // comments and trailing commas for parsing
cleaned = re.sub(r"//.*$", "", raw, flags=re.MULTILINE)
cleaned = re.sub(r",(\s*[}\]])", r"\1", cleaned)
settings = json.loads(cleaned)
dirty = False
# chat.skillsLocations
skills = settings.get("chat.skillsLocations", {})
skills_source_key = f"{source_tilde}/skills"
if skills.get(skills_source_key) is not True:
skills["~/.copilot/skills"] = True
skills[skills_source_key] = True
settings["chat.skillsLocations"] = skills
print(f" SET chat.skillsLocations")
dirty = True
else:
print(f" OK chat.skillsLocations")
# chat.useCustomizationsInParentRepositories
if settings.get("chat.useCustomizationsInParentRepositories") is True:
print(" OK chat.useCustomizationsInParentRepositories = true")
else:
settings["chat.useCustomizationsInParentRepositories"] = True
print(" SET chat.useCustomizationsInParentRepositories = true")
dirty = True
# chat.useNestedAgentsMdFiles
if settings.get("chat.useNestedAgentsMdFiles") is True:
print(" OK chat.useNestedAgentsMdFiles = true")
else:
settings["chat.useNestedAgentsMdFiles"] = True
print(" SET chat.useNestedAgentsMdFiles = true")
dirty = True
if dirty:
with open(settings_path, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=4, ensure_ascii=False)
f.write("\n")
print(f" Wrote {settings_path}")
PYTHON_SCRIPT
else
printf ' python3 not found or settings.json missing. Add these manually:\n\n'
cat <<EOF
"chat.skillsLocations": {
"~/.copilot/skills": true,
"${SOURCE_TILDE}/skills": true
},
"chat.useCustomizationsInParentRepositories": true,
"chat.useNestedAgentsMdFiles": true
EOF
printf '\n Settings file: %s\n' "$VSCODE_SETTINGS"
fi
printf '\nDone.\n'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment