[FEATURE] Support .claude/skills.local/ for project-scoped, uncommitted skills

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments ยท opened Jul 25, 2026

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. .gitignore is 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/exclude is 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.json already 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/ + .gitignore entry โ€” what I do today. Works, but requires a public edit to keep something private, has no safe default, and the skills/ vs skills/* negation gotcha fails silently in the direction of leaking the file.
  • local: true in 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.

  1. 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.
  2. I cannot commit it โ€” the repo has outside contributors, and the hostnames are not mine to publish.
  3. 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.
  4. So it lives in .claude/skills/local-release/ plus a .gitignore line I had to push, which tells everyone the skill exists.
  5. 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.

View original on GitHub โ†—