Using typemux-cc with git worktrees and late .venv creation
Git worktrees are common in AI-assisted development workflows. typemux-cc solves the problem of creating .venvafter opening files in a new worktree — no Claude Code restart required.
Open my-project-worktree/src/main.py in Claude Code.What happens:
typemux-cc searches for .venv in parent directories
Finds none (stops at git toplevel)
Caches venv=None for this document
LSP requests return errors (strict venv mode)
Copy
// From src/venv.rs:34-96pub async fn find_venv( file_path: &Path, git_toplevel: Option<&Path>,) -> Result<Option<PathBuf>, VenvError> { let mut current = file_path.parent(); while let Some(dir) = current { // Stop if we exceed git toplevel if let Some(toplevel) = git_toplevel { if !dir.starts_with(toplevel) { break; } } // Check for .venv/pyvenv.cfg let venv_path = dir.join(".venv"); let pyvenv_cfg = venv_path.join("pyvenv.cfg"); if pyvenv_cfg.exists() { return Ok(Some(venv_path)); } current = dir.parent(); } Ok(None) // Not found}
3
Create .venv
Copy
cd my-project-worktreeuv sync
Now .venv exists:
Copy
my-project-worktree/├── .venv/│ ├── bin/│ └── pyvenv.cfg # This is what typemux-cc looks for└── src/main.py
4
Reopen the file
Close and reopenmy-project-worktree/src/main.py.What happens:
textDocument/didOpen triggers a fresh .venv search
typemux-cc finds my-project-worktree/.venv
Spawns a new backend with VIRTUAL_ENV=my-project-worktree/.venv
Restores the document to the new backend
LSP features now work
Copy
// From src/proxy/document.rs:65-93pub(crate) async fn handle_did_open( &mut self, msg: &RpcMessage, count: usize, client_writer: &mut LspFrameWriter<tokio::io::Stdout>,) -> Result<(), ProxyError> { // Search for .venv (fresh search on every didOpen) let found_venv = venv::find_venv( &file_path, self.state.git_toplevel.as_deref() ).await?; // Cache document with new venv if let Some(text_content) = &text { let doc = crate::state::OpenDocument { venv: found_venv.clone(), // New venv path cached // ... }; self.state.open_documents.insert(url.clone(), doc); } // Spawn backend if not in pool if !self.state.pool.contains(venv_path) { self.create_backend_instance(venv_path, client_writer).await?; }}
No Claude Code restart required. The backend pool dynamically adds the new worktree’s backend.
typemux-cc caches the git repository root at startup:
Copy
// From src/proxy/mod.rs:48-49pub async fn run(&mut self) -> Result<(), ProxyError> { // Get and cache git toplevel self.state.git_toplevel = venv::get_git_toplevel(&cwd).await?; // ...}
The git toplevel is used as a boundary when searching for .venv:
Copy
// From src/venv.rs:8-32pub async fn get_git_toplevel(working_dir: &Path) -> Result<Option<PathBuf>, VenvError> { let output = Command::new("git") .args(["rev-parse", "--show-toplevel"]) .current_dir(working_dir) .output() .await?; if output.status.success() { let path_str = String::from_utf8_lossy(&output.stdout); let path = PathBuf::from(path_str.trim()); Ok(Some(path)) } else { Ok(None) // Not in a git repository }}
Why use git toplevel?
Prevents searching outside the project. Without a boundary, typemux-cc might find .venv from a parent project (e.g., system-wide venv), which would be incorrect.
Each worktree is a separate git working directory with its own .venv:
Copy
~/projects/├── my-project/ # Main worktree│ ├── .git/ # Git metadata (linked)│ ├── .venv/ # venv for main worktree│ └── src/main.py└── my-project-worktree/ # Separate worktree ├── .git # Symlink to main .git ├── .venv/ # Independent venv └── src/main.py
When you run git rev-parse --show-toplevel in a worktree, it returns the worktree’s root (not the main repo root). This ensures .venv search is scoped correctly.
// From src/state.rs:30-37pub struct OpenDocument { pub language_id: String, pub version: i32, pub text: String, pub venv: Option<PathBuf>, // Cached on first open}
The venv field is set once when the document is opened. Subsequent LSP requests reuse this cached value without re-searching the filesystem.Rationale: Filesystem I/O (checking for .venv/pyvenv.cfg) on every hover/completion request would be prohibitively expensive.
Closing and reopening a file clears its cache entry:
Copy
// From src/proxy/document.rs:204-223pub(crate) async fn handle_did_close(&mut self, msg: &RpcMessage) { if self.state.open_documents.remove(&url).is_some() { tracing::debug!( uri = %url, remaining_docs = self.state.open_documents.len(), "Document removed from cache" ); }}
Next didOpen triggers a fresh search:
Copy
// From src/proxy/document.rs:72-73// Search for .venv (fresh search on every didOpen)let found_venv = venv::find_venv(&file_path, self.state.git_toplevel.as_deref()).await?;
If you create .venv after opening files, you must reopen those files for LSP to work. This is the one manual step required.
[INFO] didOpen received count=1 uri=file:///home/user/projects/my-project-feat-auth/src/main.py[WARN] No .venv found file=/home/user/projects/my-project-feat-auth/src/main.py depth=3# (After uv sync and reopening the file)[INFO] didOpen received count=2 uri=file:///home/user/projects/my-project-feat-auth/src/main.py[INFO] .venv found venv=/home/user/projects/my-project-feat-auth/.venv depth=0[INFO] Creating new backend instance session=2 venv=/home/user/projects/my-project-feat-auth/.venv[INFO] Spawning backend with venv backend=pyright venv=/home/user/projects/my-project-feat-auth/.venv[INFO] Sending initialize to backend venv=/home/user/projects/my-project-feat-auth/.venv[INFO] Backend initialized session=2 venv=/home/user/projects/my-project-feat-auth/.venv[INFO] Restored document session=2 uri=file:///home/user/projects/my-project-feat-auth/src/main.py