[FEATURE] Support .claude/skills.local/ for project-scoped, uncommitted skills
Preflight
This has been requested three times before โ #41967 (4 ๐), #45094, #31155. All three were closed by the stale bot (NOT_PLANNED / duplicate), none received a maintainer response, and #41967 is now locked so it cannot be commented on or reopened. Refiling per the bot's own instruction to open a new issue and reference the old one.
This issue adds two arguments the earlier ones did not make: the resident system-prompt cost of the ~/.claude/skills/ workaround, and the fact that every other config surface in Claude Code already has this exact tier.
Problem Statement
There is no way to have a skill that is both project-scoped and not committed.
The two discovery locations force a choice between them:
.claude/skills/<name>/SKILL.mdโ project-scoped, but lands in the shared repo.~/.claude/skills/<name>/SKILL.mdโ private, but loads in every project.
The second is not a workaround, it's a different feature. Every loaded skill keeps its description resident in the system prompt on every turn โ Claude Code's own /doctor-style guidance warns about exactly this ("loaded but never invoked. Each one adds to the system prompt every turn"). So parking a project-specific skill in the user directory taxes every unrelated project, forever, for a skill needed in one repo.
That leaves .gitignore, which has its own problems:
- Hiding something personal requires a public commit.
.gitignoreis a shared, checked-in file. To keep a private skill out of the repo I have to push a line describing it to my teammates. - Naming conventions need team buy-in.
.claude/skills/local-*/only works if everyone agrees to the prefix. - The whitelist form has a silent footgun.
.claude/skills/+!.claude/skills/shared/does nothing โ git never descends into an ignored directory, so the negation is a no-op. Write.claude/skills/*and it works. Get it wrong and the failure mode is "your private skill got committed", discovered after the push. .git/info/excludeis genuinely private but does not survive a re-clone.
Proposed Solution
Discover skills from .claude/skills.local/<name>/SKILL.md, at every directory level where .claude/skills/ is already discovered (including nested/monorepo package dirs).
- Precedence on name collision:
skills.local/>skills/>~/.claude/skills/. This mirrors the existing settings cascade, so it needs no new mental model. - Auto-gitignore on first write, exactly as
.claude/settings.local.jsonalready does today. The default should be safe without the user thinking about git at all. - Same treatment for
.claude/commands.local/and.claude/agents.local/.
Why this is a gap, not a preference
Claude Code already ships a "personal, not committed" tier for every other config surface. Skills, commands and agents are the only ones missing it:
| Surface | Project (committed) | Personal (not committed) |
|---|---|---|
| Instructions | CLAUDE.md | CLAUDE.local.md โ
|
| Settings | .claude/settings.json | .claude/settings.local.json โ
(auto-gitignored) |
| MCP servers | .mcp.json | ~/.claude.json โ projects["<cwd>"] โ
|
| Skills | .claude/skills/ | โ |
| Commands | .claude/commands/ | โ |
| Subagents | .claude/agents/ | โ |
The three missing rows are also the three most likely to contain something you cannot commit โ they hold procedures, hostnames and paths, not just flags.
The convention is already taught. Users who have learned CLAUDE.local.md and settings.local.json guess skills.local/ on their own โ that's visibly what happened in #41967 and #45094, filed independently a week apart.
Alternative Solutions
.claude/skills/+.gitignoreentry โ what I do today. Works, but requires a public edit to keep something private, has no safe default, and theskills/vsskills/*negation gotcha fails silently in the direction of leaking the file.local: truein SKILL.md frontmatter (proposed in #45094) โ solves discovery but not git: the file still sits in a tracked directory and still needs an ignore rule. A separate directory is what makes it ignorable in one line.~/.claude/skills/for everything personal โ see above; the per-turn system-prompt cost applies in every repo, and there is no way to scope it.
Priority
Medium - Would be very helpful
Feature Category
Configuration and settings
Use Case Example
Contract work, two repos, one laptop.
- Client A's repo has a release procedure involving their internal Jenkins host, a Jira project key, and a staging hostname. It's a perfect skill: multi-step, easy to get wrong, needs to be written down once.
- I cannot commit it โ the repo has outside contributors, and the hostnames are not mine to publish.
- I cannot put it in
~/.claude/skills/โ then its description rides in the system prompt while I'm working in Client B's repo, where it is noise at best and a confidentiality problem at worst if it ever surfaces in output. - So it lives in
.claude/skills/local-release/plus a.gitignoreline I had to push, which tells everyone the skill exists. - With
.claude/skills.local/release/SKILL.md, it loads only in that repo, is gitignored by default, and nobody else learns anything about it.
The same shape shows up outside contract work: machine-specific paths in a run-style skill, an experimental skill you don't want to impose on teammates' context window, or a personal review checklist in an OSS repo you don't maintain.
Additional Context
Prior art in this repo: #41967, #45094, #31155 โ all closed without triage by the stale bot, cumulative ๐ across them suggests this isn't a single-user need.
Verified against Claude Code 2.1.220: there is no skills.local string anywhere in the binary, and no settings key for additional skill search paths, so there's no undocumented mechanism I'm missing.