Skip to content

Hook: project identity sidecar for multi-machine matching - #117

Merged
JayantDevkar merged 4 commits into
JayantDevkar:mainfrom
crazy-daddy:feature/extra-roots-hook
Oct 22, 2026
Merged

JayantDevkar merged 4 commits into
JayantDevkar:mainfrom
crazy-daddy:feature/extra-roots-hook

Conversation

@crazy-daddy

Copy link
Copy Markdown
Contributor

Summary

Adds hooks/project_identity.py, an optional SessionStart hook that writes karma-project.json into the session's project dir (~/.claude/projects/<encoded>/):

{ "git_identity": "owner/repo", "subdir": "api", "project_path": "/home/me/code/repo/api", "written_by": "hook" }

The file records which repository a project dir belongs to, independent of where the repo lives on disk. Extra session roots (#115) use it to match the same project across machines whose paths differ. Karma writes this file itself for machines running it; the hook covers machines whose ~/.claude is synced to a Karma install but that don't run Karma.

Independent of #115 and its follow-up: no shared code, merges in any order. Without #115 the file is simply not read.

Behavior

  • Stdlib only, Python 3.9+, always exits 0 with no output; each of its two git calls times out after 2 s
  • Writes atomically (per-process tmp file + os.replace) and only when the identity changed
  • Writes nothing when there is no origin remote, when the repo root can't be determined, or when the cwd lies outside the repo root, so a wrong subdir never merges a subproject into its repo root
  • Only writes into a project dir inside a projects dir; reads stdin and git output as UTF-8 (non-ASCII paths on Windows)
  • normalize_git_url is a copy of api/services/git_identity.py's; a test compiles the API's version (without importing the API package) and checks both agree

Docs

SETUP.md → Hook Configuration Reference → "Project Identity Sidecar (Multiple Machines)"

Testing

hooks/tests/test_project_identity.py: URL normalization, subdir computation (Windows paths, case, unrelated paths), end-to-end runs against a temp git repo (write, skip unchanged, no remote, non-ASCII path, non-project dir, missing dir), malformed stdin, API parity. The 2 failures in hooks/tests (test_ticket_branch_detector.py) also fail on current main on Windows.

@JayantDevkar JayantDevkar left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Read through. Small, careful hook: silent exit 0 on every path, list-form git calls with timeouts, the encoded dir taken from transcript_path so it cannot disagree with Karma's encoding, atomic write with no mkdir. No blockers; three things to fix before it lands, and it lands together with #115.

Should fix

  • normalize_git_url (hooks/project_identity.py:56-64, and the same logic in api/services/git_identity.py:71-86) collapses an Azure DevOps HTTPS URL https://dev.azure.com/org/proj/_git/repo to _git/repo, so unrelated Azure repos with the same name share one identity. Drop a _git segment before taking the last two parts, in both copies at once (the parity test requires them to agree), and add the case to the shared list.
  • The parity test's case list (hooks/tests/test_project_identity.py:31-42) only has easy shapes. Add https with credentials, ssh://git@host:2222/o/r.git with a port, a trailing slash, an uppercase .GIT suffix, and Azure.
  • SETUP.md:985-989, the "Removing a Hook" table has no row for project_identity.py.

Nits

  • SETUP.md:979: give the snippet an explicit "timeout": 10 like the other hook entries (worst case here is about 4 s of git on a synchronous SessionStart).
  • The hook never clears a stale sidecar if origin is later removed, and a change to the three key fields overwrites any extra fields. Fine, but say so in the doc, and confirm the four keys match what #115's indexer writes.

No CI covers hooks/, so none of the new tests have run anywhere yet; I'll add hooks/** to a workflow on main. Until then please run pytest hooks/tests on 3.9 before pushing the next revision.

Independent of the extra-session-roots stack (feature/extra-roots-core
and feature/extra-roots-coverage); can merge in any order.

SessionStart hook that writes karma-project.json (normalized git remote,
subdir within the repo, project path) into the session's project dir.
Machines whose ~/.claude is synced to a Karma install, but that don't run
Karma themselves, use it so their projects match by git identity instead
of by path. Stdlib only, Python 3.9+, never raises, writes atomically and
only when the identity changed.
Review follow-up for JayantDevkar#117.

- normalize_git_url (hook copy and api/services/git_identity.py): drop
  the Azure DevOps `_git` path segment before taking owner/repo, so
  https://dev.azure.com/org/proj/_git/repo yields proj/repo instead of
  _git/repo; strip the .git suffix case-insensitively.
- Parity test cases: https with credentials, ssh:// with a port,
  trailing slash, uppercase .GIT, Azure (https and scp-style). The api
  parser tests get the same cases.
- SETUP.md: explicit "timeout": 10 on the hook entry, a "Removing a
  Hook" row, and notes that the hook never clears a stale sidecar when
  origin is removed and overwrites extra keys when the identity changes.
… before the repo

Two defects in the previous commit's normalize_git_url, in both copies:

- The scp-style regex also matched scheme URLs with a port
  (ssh://git@host:2222/repo.git parsed as 2222/repo). Scheme URLs now
  go through the URL regex only, so that input is rejected.
- Removing every `_git` segment lost a repo literally named _git. Only a
  `_git` directly before the repo (Azure DevOps) is dropped now, so
  https://github.com/owner/_git yields owner/_git.

Parity and api tests cover both.
@crazy-daddy
crazy-daddy force-pushed the feature/extra-roots-hook branch from f342842 to 309a449 Compare October 21, 2026 19:17
@crazy-daddy

Copy link
Copy Markdown
Contributor Author

Rebased onto main and pushed two commits: 22e4fb7 for the review, 309a449 for two defects a second pass found in the same function.

Should fix

  • Azure DevOps: normalize_git_url drops a _git segment in both copies, but only in the position before the repo (parts[-2]), so a repo or owner literally named _git still parses. https://dev.azure.com/org/Proj/_git/Repo, https://org@dev.azure.com/org/Proj/_git/Repo, https://org.visualstudio.com/DefaultCollection/Proj/_git/Repo and git@ssh.dev.azure.com:v3/org/Proj/Repo all give proj/repo.
  • Parity cases now include https with credentials, ssh://git@host.example.com:2222/Owner/Repo.git, trailing slash, .GIT, Azure https and ssh, and https://github.com/Owner/_git. The .GIT case needed a case-insensitive suffix strip in both copies.
  • While adding the port case I found the scp regex matched scheme URLs with a port first (ssh://git@host:2222/Owner/Repo.git parsed as path 2222/Owner/Repo and only passed because the last two segments are taken). Scheme URLs now go to _URL_RE first; ssh://git@host:2222/repo.git gives None instead of 2222/repo. Both copies, parity test covers it.
  • SETUP.md: "Removing a Hook" row for project_identity.py.

Nits

  • Snippet has "timeout": 10; the prose says the two git calls self-limit to 2 s each so the entry is a safety net.
  • Documented: the hook never clears a stale sidecar after origin is removed, and a change to any of the three key fields rewrites the file, dropping extra keys.
  • Confirmed the four keys (git_identity, subdir, project_path, written_by) match what write_sidecar in Merge sessions from other machines via extra session roots (1/2: core) #115 writes; only written_by differs by value (hook vs karma-api).

Verified on Python 3.9.13: pytest hooks/tests/test_project_identity.py 39 passed. api/tests/test_git_identity.py 31 passed on the api venv. ruff check/format --check clean with the repo config. The two test_ticket_branch_detector.py failures in hooks/tests fail identically on main on this Windows box and are untouched here.

…ir; honest temp-file assertion

The sidecar now requires the transcript's grandparent to resolve to CLAUDE_CONFIG_DIR (or ~/.claude) /projects. The temp-file test globs the pid-suffixed name so it can fail.

Claude-Session: https://claude.ai/code/session_019dHRodCxwuwTvnKtUNrUU7
@JayantDevkar

Copy link
Copy Markdown
Owner

Re-reviewed the two commits against the code: every item in your reply is as described (Azure _git only at parts[-2], scheme URLs before scp, case-insensitive .git, parity cases, SETUP row, timeout note). Both copies of normalize_git_url are logically identical.

Pushed 031d65f on top with three small things from the second read, so you don't have to round-trip:

  • The sidecar guard now requires the transcript's parent to resolve to <config dir>/projects (CLAUDE_CONFIG_DIR, else ~/.claude), not any directory whose parent happens to be named projects. New end-to-end test for the outside-config case.
  • The temp-file assertion could never fail (the real name carries a pid suffix); it now globs .karma-project.json.tmp*.
  • One-line comment on the ssh://git@host:2222/_git/repo case saying the _git drop is positional.

40 + 31 tests pass, ruff clean. Lands together with #115 once that one's finishing commit is in.

@JayantDevkar JayantDevkar left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approved with #115. Hook is 3.9-safe, every path exits 0 silently, both normalize_git_url copies are logically identical and the parity test covers the edge list. Merging after #115.

@JayantDevkar
JayantDevkar merged commit 487f702 into JayantDevkar:main Oct 22, 2026
28 of 29 checks passed
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.

2 participants