Skip to content

feat(project-name): CLAUDE_MEM_PROJECT_NAME_SOURCE=git-remote for stable org/repo slugs#2827

Open
surfingdoggo wants to merge 1 commit into
thedotmack:mainfrom
surfingdoggo:feat/project-name-from-remote
Open

feat(project-name): CLAUDE_MEM_PROJECT_NAME_SOURCE=git-remote for stable org/repo slugs#2827
surfingdoggo wants to merge 1 commit into
thedotmack:mainfrom
surfingdoggo:feat/project-name-from-remote

Conversation

@surfingdoggo

Copy link
Copy Markdown

Problem

claude-mem derives the project name from the folder / git-root basename. That name silently changes if a checkout directory is renamed, and two unrelated repos that happen to share a folder name collide into the same project — scattering or merging memory incorrectly.

Fix

Add an opt-in setting CLAUDE_MEM_PROJECT_NAME_SOURCE (default path). When set to git-remote, the project name becomes a stable org/repo slug parsed from the git origin remote URL — stable across directory renames and identical across a repo's worktrees (they share the remote).

  • Default path is a no-op for existing users — behavior is unchanged unless you opt in.
  • In git-remote mode, worktree parent/child compositing is skipped (a repo's worktrees share its origin and already resolve to one slug).
  • parseOriginUrlToSlug is a pure, exported helper covering scp-style (git@host:org/repo.git), https://, and ssh:// forms, optional .git/trailing slash, and nested subgroups — with unit tests.

Changes

  • New setting + resolver (src/utils/project-name.ts, src/shared/SettingsDefaultsManager.ts).
  • Unit tests for parseOriginUrlToSlug.
  • Documented the setting in the configuration reference.

Note on CI

main currently has unrelated pre-existing failures (PID-file / process-manager and logger-standards tests, plus a duplicate ModeManager import typecheck error). This change is isolated to project-name resolution; the project-name test suite passes (29/29).

Relates to upstream discussion on stable project identity (#2663, #2711).

🤖 Generated with Claude Code

… stable org/repo slugs

By default the project name is derived from the folder/git-root basename,
which silently changes if a checkout directory is renamed and collides when
two different repos share a folder name. This adds an opt-in setting,
CLAUDE_MEM_PROJECT_NAME_SOURCE (default 'path'), that when set to 'git-remote'
derives a stable org/repo slug from the git origin remote URL instead.

- Default 'path' preserves existing behavior exactly (no-op for current users).
- In git-remote mode, getProjectContext skips parent/child worktree compositing,
  since a repo's worktrees share its origin and already resolve to one slug.
- parseOriginUrlToSlug is a pure, exported helper (scp-style, https, ssh://,
  optional .git/trailing slash, nested subgroups) with unit tests.
- Documented in the configuration reference.

Relates to upstream discussion on stable project identity (thedotmack#2663, thedotmack#2711).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an opt-in CLAUDE_MEM_PROJECT_NAME_SOURCE=git-remote mode that derives the project name from the origin remote URL instead of the directory basename, making project identity stable across renames and consistent across worktrees.

  • parseOriginUrlToSlug is added as a pure, exported helper handling scp-style, https://, and ssh:// forms, with a regex ordering bug where a URL containing both .git and a trailing slash (e.g. https://host/org/repo.git/) is not correctly cleaned, leaving .git in the resulting slug.
  • getProjectContext skips worktree parent/child compositing in git-remote mode, which also applies when the slug falls back to a basename (no origin remote configured) — a subtle behavioral edge case.
  • The SettingsDefaultsManager and config docs are updated cleanly with a sensible path default preserving existing behavior.

Confidence Score: 3/5

Safe to merge for existing users (default path mode is unchanged), but the git-remote mode has a correctness bug that would silently produce wrong project names for repositories whose configured remote URL ends with both .git and a trailing slash.

The regex replacement order in parseOriginUrlToSlug means .git/-suffixed URLs are not fully cleaned, producing slugs like org/repo.git instead of org/repo. This is an opt-in feature so existing users are unaffected, but anyone enabling git-remote mode with such URLs will get incorrect — and potentially colliding — project identities. The fix is a one-line swap and worth landing before the feature is widely adopted.

src/utils/project-name.ts — specifically the parseOriginUrlToSlug regex chain and the getProjectContext early-return that skips worktree compositing even when no remote slug was resolved.

Important Files Changed

Filename Overview
src/utils/project-name.ts Adds parseOriginUrlToSlug, deriveSlugFromRemote, and useRemoteProjectName helpers; integrates the opt-in into getProjectName and getProjectContext. Has a regex ordering bug that leaves .git in the slug when the URL also has a trailing slash.
src/shared/SettingsDefaultsManager.ts Adds CLAUDE_MEM_PROJECT_NAME_SOURCE to the interface and defaults map with a sensible default of 'path'. Change is minimal and correct.
tests/utils/project-name.test.ts Adds 9 unit tests for parseOriginUrlToSlug covering major URL forms. Missing a test for the .git+trailing-slash combination that would expose the regex ordering bug.
docs/public/configuration.mdx Adds a new row to the configuration reference table documenting the new CLAUDE_MEM_PROJECT_NAME_SOURCE env var. Documentation is clear and accurate.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["getProjectName(cwd)"] --> B{"cwd empty?"}
    B -- yes --> C["return 'unknown-project'"]
    B -- no --> D["findGitRepoRoot"]
    D --> E{"repoRoot found?"}
    E -- no --> F["basename of cwd"]
    E -- yes --> G{"useRemoteProjectName?"}
    G -- no --> H["basename of repoRoot"]
    G -- yes --> I["deriveSlugFromRemote"]
    I --> J{"slug parsed OK?"}
    J -- yes --> K["return org/repo slug"]
    J -- no --> H

    L["getProjectContext(cwd)"] --> M{"cwd null?"}
    M -- yes --> N["return cwdProjectName, no worktree info"]
    M -- no --> O["detectWorktree"]
    O --> P{"useRemoteProjectName?"}
    P -- yes --> Q["return cwdProjectName, parent=null, skip compositing"]
    P -- no --> R{"isWorktree and parentName?"}
    R -- yes --> S["return composite parent/child name"]
    R -- no --> T["return cwdProjectName"]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/utils/project-name.ts:53
The `.git` suffix replacement runs before the trailing-slash strip, so a URL ending with `.git/` (e.g. `https://github.com/org/repo.git/`) is left with `.git` in the final slug (`org/repo.git`). Reversing the two replacements — strip trailing slashes first, then `.git` — fixes the combination.

```suggestion
  const cleaned = url.trim().replace(/\/+$/, '').replace(/\.git$/, '');
```

### Issue 2 of 2
src/utils/project-name.ts:56-63
**URL-form fallback produces garbage slugs for bare-host URLs**

When a URL doesn't match the scheme+host+slash pattern (e.g. a `file:///home/user/repo` local remote or a URL with no path like `https://github.com`), the `replace` call is a no-op and the full raw string becomes `pathPart`. Splitting `file:///home/user/repo` by `/` yields `['file:', 'home', 'user', 'repo']`, so `slice(-2)` returns `user/repo` — a silently wrong slug derived from filesystem path components instead of project identity. Since `deriveSlugFromRemote` only returns `null` on exec failure, not on parse failure, there is no safety net; the bad slug propagates back to `getProjectName`.

Reviews (1): Last reviewed commit: "feat(project-name): add CLAUDE_MEM_PROJE..." | Re-trigger Greptile

Comment thread src/utils/project-name.ts
*/
export function parseOriginUrlToSlug(url: string): string | null {
if (!url || !url.trim()) return null;
const cleaned = url.trim().replace(/\.git$/, '').replace(/\/+$/, '');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The .git suffix replacement runs before the trailing-slash strip, so a URL ending with .git/ (e.g. https://github.com/org/repo.git/) is left with .git in the final slug (org/repo.git). Reversing the two replacements — strip trailing slashes first, then .git — fixes the combination.

Suggested change
const cleaned = url.trim().replace(/\.git$/, '').replace(/\/+$/, '');
const cleaned = url.trim().replace(/\/+$/, '').replace(/\.git$/, '');
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/project-name.ts
Line: 53

Comment:
The `.git` suffix replacement runs before the trailing-slash strip, so a URL ending with `.git/` (e.g. `https://github.com/org/repo.git/`) is left with `.git` in the final slug (`org/repo.git`). Reversing the two replacements — strip trailing slashes first, then `.git` — fixes the combination.

```suggestion
  const cleaned = url.trim().replace(/\/+$/, '').replace(/\.git$/, '');
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/utils/project-name.ts
Comment on lines +56 to +63
const pathPart = scp
? scp[1]
// URL form: scheme://host/org/repo → strip scheme + host.
: cleaned.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+\//i, '');

const segments = pathPart.split('/').filter(Boolean);
if (segments.length >= 2) return segments.slice(-2).join('/');
if (segments.length === 1) return segments[0];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 URL-form fallback produces garbage slugs for bare-host URLs

When a URL doesn't match the scheme+host+slash pattern (e.g. a file:///home/user/repo local remote or a URL with no path like https://github.com), the replace call is a no-op and the full raw string becomes pathPart. Splitting file:///home/user/repo by / yields ['file:', 'home', 'user', 'repo'], so slice(-2) returns user/repo — a silently wrong slug derived from filesystem path components instead of project identity. Since deriveSlugFromRemote only returns null on exec failure, not on parse failure, there is no safety net; the bad slug propagates back to getProjectName.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/project-name.ts
Line: 56-63

Comment:
**URL-form fallback produces garbage slugs for bare-host URLs**

When a URL doesn't match the scheme+host+slash pattern (e.g. a `file:///home/user/repo` local remote or a URL with no path like `https://github.com`), the `replace` call is a no-op and the full raw string becomes `pathPart`. Splitting `file:///home/user/repo` by `/` yields `['file:', 'home', 'user', 'repo']`, so `slice(-2)` returns `user/repo` — a silently wrong slug derived from filesystem path components instead of project identity. Since `deriveSlugFromRemote` only returns `null` on exec failure, not on parse failure, there is no safety net; the bad slug propagates back to `getProjectName`.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant