pingdotgg / pingdotgg/t3code

[Bug]: Every user-scope Claude skill is labelled "Project Skill" — packaged backendCwd is $HOME, so <cwd>/.claude/skills collides with the user config root

Open
#8,757 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Before submitting
  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Searched: #6449, #7871, #3040, #5487, #4544. Those cover which skills are discovered. This is about the scope they are discovered with, which is wrong for every packaged desktop install.

Area

apps/server

Summary

In a packaged desktop build, every user-scope Claude skill is reported as scope: "project" and rendered in the $ picker with a "Project Skill" badge. The badge is the only scope signal in the picker, and it is wrong for 100% of entries on a machine with no repo-local skills.

Steps to reproduce
  1. Have Claude skills in ~/.claude/skills/ and no .claude/skills or .agents/skills in the open repository.
  2. Open T3 Code desktop (packaged), Claude provider.
  3. Type $ in the composer. Every row is badged Project Skill.
  4. Confirm in the provider cache:
python3 -c "
import json,os,collections
d=json.load(open(os.path.expanduser('~/.t3/caches/claudeAgent.json')))
f=[]
def w(o):
    if isinstance(o,dict):
        for k,v in o.items():
            if k=='skills' and isinstance(v,list): f.append(v)
            else: w(v)
    elif isinstance(o,list): [w(x) for x in o]
w(d)
print(len(f[0]), collections.Counter(s.get('scope') for s in f[0]))
print(f[0][0])
"

Output on both of my machines: every entry scope='project', every path under ~/.claude/skills/.

Cause

apps/desktop/dist-electron/main.cjs starts the backend in the user's home directory whenever the app is packaged:

backendCwd: input.isPackaged ? homeDirectory : appRoot,

That value becomes process.cwd() for the server, which becomes ServerConfig.cwd, which ClaudeDriver reads and passes straight to discovery:

const { cwd } = yield* ServerConfig;
...
const skills = yield* discoverClaudeSkills(claudeSettings, cwd, resolvedEnvironment);

discoverClaudeSkills then builds its roots from that same cwd:

const roots = [
  { directory: path.join(configDirPath, "skills"), scope: "user" },
  ...cwd ? [{ directory: path.join(cwd, ".agents", "skills"), scope: "project" },
            { directory: path.join(cwd, ".claude", "skills"), scope: "project" }] : []
];

With cwd === $HOME, root 3 resolves to $HOME/.claude/skills — byte-for-byte the same directory as root 1. The loop writes into a Map keyed by skill name and lets later roots win, so every user skill is re-written with scope: "project". Root 2 does the same to ~/.agents/skills, the Agent Skills standard user directory.

Verified against the shipped app.asar of 0.0.36, which is identical to main here.

Verified on two machines

Both after a clean quit and relaunch, so the cache is freshly built:

machine backend process cwd skills cached scope tally
MacBook /Users/<user> 44 {'project': 44}
Mac mini /Users/<user> 40 {'project': 40}

Neither repository contains .claude/skills or .agents/skills. Every one of those 84 labels is wrong.

Expected behavior

Skills found in the user config directory report scope: "user" and render without the "Project Skill" badge.

Actual behavior

They report scope: "project" and render as "Project Skill", so the picker cannot distinguish a genuine repo-local skill from a global one — the exact distinction the badge exists to make.

Proposed guard

Independent of the larger question in #6449 of which cwd the probe should use, this specific collision can be closed in discoverClaudeSkills by skipping a project root that resolves to the user root:

const userRoot = path.resolve(path.join(configDirPath, "skills"));
const roots = [
  { directory: path.join(configDirPath, "skills"), scope: "user" },
  ...(cwd ? [
    { directory: path.join(cwd, ".agents", "skills"), scope: "project" },
    { directory: path.join(cwd, ".claude", "skills"), scope: "project" },
  ].filter((root) => path.resolve(root.directory) !== userRoot) : []),
];

That fixes every packaged install without changing which skills are found.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Alpha) 0.0.36, packaged desktop. Cause confirmed present in current main.

Environment

macOS 26.5.2 (Apple Silicon), two machines, Claude provider, claude CLI 2.1.251 and 2.1.250.

Workaround

None. The badge cannot be corrected from settings.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in apps/server by locating discoverClaudeSkills and the roots built from ServerConfig.cwd; compare the user skills root with the project roots when cwd is the home directory. Check the packaged entry point in apps/desktop/dist-electron/main.cjs and reproduce the cache behavior described in the issue. Done means user-directory skills retain scope: "user" while genuine repository skills remain project-scoped.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend, desktop
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.