How typemux-cc works automatically with Claude Code
typemux-cc runs as a Claude Code plugin and works automatically in the background. You don’t need to manually start or configure it for basic operation.
The plugin acts as a transparent proxy between Claude Code and your Python type-checking backend (pyright, ty, or pyrefly). It automatically detects .venv directories and spawns backend processes as needed.
1
Plugin starts with Claude Code
When Claude Code launches, typemux-cc starts automatically. It searches for a fallback .venv in:
Git repository root (if in a git repo)
Current working directory
If neither exists, starts with an empty backend pool
Copy
// From src/venv.rs:99-155pub async fn find_fallback_venv(cwd: &Path) -> Result<Option<PathBuf>, VenvError> { // 1. Get git toplevel let git_toplevel = get_git_toplevel(cwd).await?; // 2. Search for .venv from toplevel if let Some(toplevel) = &git_toplevel { let venv_path = toplevel.join(VENV_DIR); let pyvenv_cfg = venv_path.join(PYVENV_CFG); if pyvenv_cfg.exists() { return Ok(Some(venv_path)); } } // 3. Search for .venv from cwd let venv_path = cwd.join(VENV_DIR); let pyvenv_cfg = venv_path.join(PYVENV_CFG); if pyvenv_cfg.exists() { return Ok(Some(venv_path)); } Ok(None)}
2
Opening Python files triggers detection
When you open a Python file, typemux-cc:
Searches for .venv by traversing parent directories from the file location
Stops at git toplevel (repository boundary) if in a git repo
Verifies that .venv/pyvenv.cfg exists (strict validation)
Spawns a backend if .venv is found and not already in the pool
Copy
pub async fn find_venv( file_path: &Path, git_toplevel: Option<&Path>,) -> Result<Option<PathBuf>, VenvError> { // Start from file's parent directory let mut current = file_path.parent(); let mut depth = 0; 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 existence let venv_path = dir.join(VENV_DIR); let pyvenv_cfg = venv_path.join(PYVENV_CFG); if pyvenv_cfg.exists() { tracing::info!( venv = %venv_path.display(), depth = depth, ".venv found" ); return Ok(Some(venv_path)); } current = dir.parent(); depth += 1; } Ok(None)}
3
Backend spawns with correct environment
Each backend process spawns with:
VIRTUAL_ENV set to the detected .venv path
PATH prefixed with .venv/bin
Unique session ID for tracking
Copy
// From src/backend.rs:46-53pub fn apply_env(&self, cmd: &mut Command, venv: &Path) { let venv_str = venv.to_string_lossy(); cmd.env("VIRTUAL_ENV", venv_str.as_ref()); let current_path = std::env::var("PATH").unwrap_or_default(); let new_path = format!("{}/bin:{}", venv_str, current_path); cmd.env("PATH", &new_path);}
These environment variables are only set for the backend process. Your shell environment and system PATH remain unchanged.
When you open the first Python file from a project (directory with .venv), typemux-cc searches for .venv and spawns a backend if found.
Switching to a different project
Opening a file from a project with a different .venv path spawns a new backend. The previous backend stays alive in the pool.
Late .venv creation
If you create .venv after opening files, reopen the file to trigger detection. Cached documents don’t automatically re-search for .venv.
If .venv doesn’t exist when you open a file, typemux-cc caches venv=None for that document. Creating .venv later won’t take effect until you close and reopen the file.
When a new backend spawns, it needs to know about already-open files. typemux-cc automatically resends textDocument/didOpen for all documents belonging to that .venv.
Copy
// From src/proxy/initialization.rs:179-266pub(crate) async fn restore_documents_to_backend( &self, backend: &mut LspBackend, venv: &Path, session: u64, _client_writer: &mut LspFrameWriter<tokio::io::Stdout>,) -> Result<(), ProxyError> { for (url, doc) in &self.state.open_documents { // Only restore documents matching this venv let should_restore = doc.venv.as_deref() == Some(venv); if !should_restore { skipped += 1; continue; } // Resend didOpen with cached text let didopen_msg = RpcMessage { method: Some("textDocument/didOpen".to_string()), params: Some(serde_json::json!({ "textDocument": { "uri": url.to_string(), "languageId": doc.language_id, "version": doc.version, "text": doc.text, } })), // ... }; backend.send_message(&didopen_msg).await?; }}
2
Incremental text updates
textDocument/didChange events update the cached text immediately, even if no backend is running for that document.
Copy
// From src/proxy/document.rs:136-201pub(crate) async fn handle_did_change(&mut self, msg: &RpcMessage) { for change in changes_array { if let Some(range) = change.get("range") { // Incremental change (LSP range-based edit) crate::text_edit::apply_incremental_change( &mut doc.text, range, new_text )?; } else if let Some(new_text) = change.get("text") { // Full text replacement doc.text = new_text.to_string(); } } if let Some(v) = version { doc.version = v; }}
3
Cache invalidation on close
When you close a file, typemux-cc removes it from the cache.
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" ); }}
Important: The .venv path is cached when a document is first opened. If you:
Open a file (no .venv exists yet)
Create .venv later
Try to use LSP features
You’ll get errors because the cached venv=None is reused. Solution: Close and reopen the file to trigger a fresh .venv search.
Why this design? Re-searching for .venv on every LSP request would be expensive (filesystem I/O). Caching trades off flexibility for performance. The workaround (reopen files) is simple and rare in practice.