fix: resolve the login pin from a git worktree #46
Reference in New Issue
Block a user
Delete Branch "fix/worktree-login-pin"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #24.
Stacked PR — base is
fix/no-store-for-label-payloads, notmain. Itbuilds on the repo-root resolution that branch added to
_gitea.py, so reviewor merge that one first.
The bug
_gitea.require_logincalledfind_pin(os.getcwd())and walked up from thereand nowhere else. A git worktree is a sibling of the main checkout, not a
descendant, and
.claude/settings.local.jsonis untracked — so the pin sits inthe main checkout, is not on the worktree's parent chain, and every script that
talks to Gitea (
pull.py,push.py,remote.py,comment.py,labels.py,wiki_*.py) died inside a worktree with "no login pinned". In the samedirectory
teaitself worked: the guard hook had a second, different copy ofthe search.
The fix
The search order is written once, in
skills/auth/scripts/pin.py, and bothcallers import it —
_gitea.require_loginandhooks/tea-guard.sh:$CLAUDE_PROJECT_DIRcwd; scriptspass nothing)
Each start directory is searched up its parent chain, and only if that finds
nothing, up the parent chain of the main working tree of any linked worktree
met on the way — reached by reading
gitdir:out of the.gitfile andfollowing
commondir. Files, notgit rev-parse: a PreToolUse hook runs beforeevery Bash call and should not fork to answer this.
The search still starts at the working directory, never at
__file__— theconstraint in #24. The asymmetry with
issue.store_rootand_gitea.PAYLOAD_ROOT, which are anchored on their own module, is deliberateand now documented in
pin.pyandAGENTS.md: where an installation keeps itsfiles is a fact about the installation, whose login a project runs under is a
fact about the project, and a plugin pointed at somebody else's tree must not
answer the second from its own directory.
The identity layer is the home for it because both callers may import it
without crossing a layer: no domain learns what a login is, and the bridge
does not have to reach into
skills/issuefor walk-up logic.Consequences:
/tea:authnever needs running inside a worktree, so no secondsettings.local.jsonlands in a directory that disappears with the branch (theskill now says to write it beside the common
.git); andpush.pycan runwhere the work is, so
branch:→refis the worktree's branch instead of themain checkout's, which the old cwd workaround got wrong.
Tests
tests/test_login_pin.py, 20 cases, stdlibunittest, temp dirs, no network:the hop against a hand-built layout and against a real
git worktree add; async script run from the worktree finding the login; no pin anywhere still
erroring; the scripts' own directory not being a pin source;
push.pyfrom theworktree sending
ref: feature; the hook and a script answering the samedirectory alike; and two mechanical checks that neither caller keeps a second
copy of the walk.
Three existing fixtures now copy
skills/auth/scriptsbecause the transportimports it.
python3 -m unittest discover -s tests— 264 pass, up from 244 onthe base branch, nothing regressed.
Verified by hand from the worktree with
CLAUDE_PROJECT_DIRunset: bothremote.pyandhooks/tea-guard.shresolve the pin out of the main checkout.🤖 Generated with Claude Code