Repository navigation
Make kitaru setup safe to re-run against Claude Code and custom uv environments - #1004
Merged
Merged
Conversation
Four reliability gaps in `kitaru setup`, found in the review of #977 after it merged: - The Claude Code readback only checked that the executable appeared in `claude mcp get` output, so an older entry keeping its old --server and --mode still passed as success. The output is now parsed and must carry exactly our command and arguments; a failed or unparseable readback is a failure, not a pass. - The existing Claude entry was removed before the replacement was added, with nothing put back when the add failed. The removed entry is now restored, and only when it was in our scope, so a shadowing entry from another scope is never copied into ours. An entry that already matches is left untouched. - Projects using UV_PROJECT_ENVIRONMENT were treated as tool installs because only an environment literally named `.venv` counted, which sent Cursor's entry to the global file. The project is now found by walking up from the working directory to the pyproject whose uv environment is the running interpreter. - A skill directory swap that failed at the final rename left the active skill hidden in a retired `.old` directory. The previous version is now moved back on failure. Each case has a regression test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QAUAys8r9ZkczYYGnnJ5ae
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
strickvl
approved these changes
Sep 7, 2026
strickvl
left a comment
Collaborator
There was a problem hiding this comment.
Tested this locally on my side as well
…ity-followups # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #977, which merged before a review of
src/kitaru/cli/setup.pyturned up four reliability gaps. All four are fixed here, each with a regression test intests/cli/test_setup.py.What changes
claude mcp getoutput is parsed into aClaudeEntry(command, args, scope) and must match our command and arguments exactly. Previously only the executable path was checked, so an older entry that kept its old--serverand--mode destructivewas reported as a successfulread-onlysetup. A readback that fails, or cannot be parsed, is now a failed step rather than a pass.claude mcp addrefuses duplicates, so the existing entry has to be removed first. If the add then fails, the removed entry is re-added. Restore only happens when the removed entry was in our scope; a shadowing entry from another scope is never copied into ours. An entry that already matches is left untouched, so re-running is a no-op instead of a remove/add cycle.UV_PROJECT_ENVIRONMENTis honored for project scope._find_project_dirused to accept only an environment literally named.venv, so a project with a custom environment was treated as a tool install and Cursor's entry landed in the global~/.cursor/mcp.jsoninstead of the project file. The project is now found by walking up from the working directory to thepyproject.tomlwhose uv environment (.venv, or the configured one, relative values resolved against the project) is the running interpreter. This also covers uv workspaces, where the environment lives at the root while the working directory is in a member._write_skillsretires the old skill directory before renaming the staged one in. If that final rename failed, the active skill vanished into a hidden.olddirectory. The old version is now moved back on failure.Reviewer Notes
The interesting code is
ClaudeCodeClient.registerinsrc/kitaru/cli/setup.py. Read it top to bottom: get, optional early return, remove, add, restore on failure, readback, parse, compare. The risk is in the ordering. If the restore ran whenremovedfailed, it would add a duplicate; if it ran for an entry from another scope, it would copy that scope's launch into ours. Both guards are on theprevious/removedpair, and the two new teststest_claude_failed_add_restores_the_previous_entryandtest_claude_failed_add_does_not_restore_an_entry_from_another_scopepin the exactclaudecall sequence._parse_claude_entryreads theCommand:,Args:, andScope:lines thatclaude mcp getprints. Args are space-joined by the CLI, so an argument with whitespace cannot round-trip; nothingkitaru setupwrites contains one. There is no loose fallback: if the output cannot be parsed, the step fails with a pointer toclaude mcp get. The line format was checked against a real Claude Code CLI by registering a throwaway stdio server: it printsCommand: /bin/echo,Args: hello world(space-joined), andScope: User config (available in all your projects)/Scope: Project config (shared via .mcp.json)/Scope: Local config (private to you in this project), so the first word of the scope line is what the parser keys on. The same check confirmed thatclaude mcp addrefuses a name that already exists in the target scope (exit 1,already exists in user config), which is why the remove-then-add sequence and its restore path exist._find_project_dirchanged from "is the prefix's parent a project" to "which ancestor of cwd owns this environment". The cwd-inside-project constraint is preserved by construction.test_resolve_mcp_launch_honors_uv_project_environmentcovers a relative and an absolute environment value plus the run-from-outside case.The test fake
_fake_claudewas rewritten to render a realisticclaude mcp getblock, since the parser now needs one. It gained knobs for the existing entry's scope, how many adds fail, and whether the readback fails.Reproduction
With a real Claude Code CLI, from a fresh shell:
Before this change the second command reported the Claude step as done while
claude mcp getstill showed the old server anddestructive. After it, setup either replaces the entry (andclaude mcp getshows--mode read-only) or reports a failed step naming the mismatch. Runkitaru setup --no-skills --mode read-onlya second time: it now makes a singleclaude mcp getcall and leaves the entry alone.For the uv environment case:
Expect
"project"and the project path, and a.cursor/mcp.jsoninside the project rather than under~. Running the same command withUV_PROJECT_ENVIRONMENTunset reports"user", which is what the previous_find_project_dirreturned in every case, so the two runs show the before and after on one branch.Local checks run: ruff format, ruff check, ty, typos, and
pytest tests/cli(558 passed).🤖 Generated with Claude Code
https://claude.ai/code/session_01QAUAys8r9ZkczYYGnnJ5ae