Operational env-var knobs and periodic maintenance for a running CodeLens
deployment. Reference material extracted from CLAUDE.md.
Semantic Edit Backend (semantic_edit_backend)
refactor_extract_function, refactor_inline_function, refactor_move_to_file, and refactor_change_signature are dual-backend tools:
tree-sitter(default) — syntactic-only, regex-style transformation. Fast, no language server required, but degraded: captured locals not detected, no scope analysis, no return-type inference.lsp— LSP-driventextDocument/codeAction+codeAction/resolvefor trueWorkspaceEditsemantics. Honors the language server's safety rules. Currentlyconditional_authoritative_apply— fixture coverage gates apply.auto— pick LSP when the file extension has a default LSP server mapping (rust/python/ts/js/go/java/kotlin, etc.), otherwise fall back to tree-sitter. Closest CodeLens equivalent of Serena's always-on LSP routing. Usesemantic_edit_backend=autoper call orCODELENS_SEMANTIC_EDIT_BACKEND=autofor the whole session.
Falls back to tree-sitter if no file_path is supplied in auto mode so capability detection never errors.
LSP Subprocess Trust Boundary
LSP tools do not treat command and args as a generic process launcher.
The engine authorizes one immutable tuple before any spawn:
commandmust identify a registeredLSP_RECIPESserver.argsmust exactly match that recipe (omittingargsselects the recipe defaults).- The executable must already be present in the session pool's canonical trust map. Path-qualified input is accepted only when it canonicalizes to that same executable.
At pool construction, trusted executables come from the daemon's inherited
PATH, conservative platform fallback directories, and
CODELENS_LSP_PATH_EXTRA. Project node_modules/.bin directories are not
searched implicitly. TypeScript and JavaScript files go to
typescript-language-server unless the TypeScript it would load has no
lib/tsserver.js (TypeScript 7, the Go port); then the trusted tsc serves
them with tsc --lsp --stdio, but only when that tsc is itself TypeScript 7
or later. register_trusted_lsp_binary exists for an embedding host
to add an explicit mapping; it is a host configuration API and must never
receive tool-call input.
Treat every directory in PATH and CODELENS_LSP_PATH_EXTRA as executable
code: it must be operator-owned and not writable by a bound project or remote
client. Restart the daemon after changing those variables so new pools capture
the intended paths. Pre-warm uses this same trust map, so it cannot widen the
launch surface.
This policy prevents direct arbitrary-command and free-form-argument execution.
It does not sandbox a trusted language server after launch; servers may load
project plugins, build scripts, proc macros, or compiler extensions. For
hostile repositories, isolate the daemon at the OS/container layer and omit
LSP tools from the exposed surface. CODELENS_LSP_PREWARM=off only disables
eager startup and is not a sandbox.
Project Binding Gate (HTTP sessions)
An HTTP session without an explicit project binding (initialize project,
the x-codelens-project header, or prepare_harness_session /
activate_project with project=) is served from the daemon's default
project, which is usually not the caller's repository.
- Content mutations on such a session are refused with
project_binding_required(-32003).CODELENS_ALLOW_UNBOUND_MUTATION=1restores advisory-only behavior. - Reads are advisory by default: the payload carries a
project_bindingblock withbound: false. Agents routinely ignored that block and trusted empty results from the wrong repository, soCODELENS_REQUIRE_EXPLICIT_BINDING=1refuses unbound reads with the same error. Session tools (prepare_harness_session,get_current_config, ...) stay callable so the caller can bind and retry.
Requests without Mcp-Session-Id
An HTTP request that carries no Mcp-Session-Id (other than initialize)
gets a session of its own that lasts for that request only. It is seeded
from the request's headers (x-codelens-project, x-codelens-client,
x-codelens-client-version, profile and host headers) and, when no header
names the client, from _meta["io.modelcontextprotocol/clientInfo"]. It is
never journaled and never returned as a session id. A
prepare_harness_session(project=...) made this way binds that request only
and says so with binding_scope: "request". Keep the binding by sending
x-codelens-project on every request or by initializing a session.
Telemetry records these requests under the session id request.
Session journal (CODELENS_SESSION_JOURNAL, default on)
HTTP sessions keep their soft state in
~/.codelens/runtime/project-writers/sessions/<session id>.json, beside the
writer leases (with CODELENS_RUNTIME_DIR set, $CODELENS_RUNTIME_DIR/sessions/):
the project binding when the caller chose it (initialize
parameter or prepare/activate), client name, version and host context, and the
requested profile. When a client comes back under the same id after the
daemon restarted or the session idled out (30 minutes), the resurrected
session gets that state back instead of falling to the daemon's default
project. Request headers still win. trusted_client is never written. Files
are owner-only, removed on an explicit DELETE, and pruned after 7 days or past
2,048 entries. CODELENS_SESSION_JOURNAL=0 keeps sessions memory-only.
A session that chose its project and calls prepare_harness_session without
project re-prepares that project, so it gets binding_status: "building"
within the bind budget like an explicit prepare, and a header binding stays a
header binding.
Bind budget (CODELENS_BIND_BUDGET_SECS, default 20)
A project's runtime (writer lease, index open, discovery refresh) is built on a
background thread. A request waits for it at most this long; past that,
prepare_harness_session answers activated: false, binding_status:
"building", retry_after_ms and binds the HTTP session to the project, and
project tools answer the retryable index_not_ready (-32004, retry after
5 s). The build keeps going, concurrent requests for the same project wait on
the same build, and the first request after it finishes installs the runtime.
This replaces the follower wait CODELENS_PROJECT_BUILD_WAIT_SECS.
Why: the daemon is re-signed ad hoc on every redeploy, so macOS treats each
build as a new app. Its first open of a file in a TCC-protected folder
(~/Downloads, ~/Documents, ~/Desktop) shows the consent dialog "‘codelens-mcp-http’
would like to access files in your Downloads folder" and the open() blocks
until someone answers it. On 2026-10-10 that held a ~/Downloads index open
for 234.8 s (connect_ms), and after the next redeploy the build thread sat in
open() with the dialog on screen. A request that builds inline holds the
client past its timeout (Claude Code gives up at 60 s).
Analysis Artifact Cache (LRU + TTL)
artifact_store keeps recent analysis results (the analysis_id values returned by review_architecture, module_boundary_report, dead_code_report, etc.) so chained calls like get_analysis_section can resolve them. Two caps with runtime overrides:
CODELENS_MAX_ANALYSIS_ARTIFACTS(non-zero usize, default50) — FIFO eviction count cap.CODELENS_ANALYSIS_TTL_HOURS(non-zero u64, default6) — TTL after which entries expire.
Invalid or 0 values fall back to the compiled defaults. Raise both when chaining many start_analysis_job calls within one session, or when a builder depends on a multi-hour-old handle.
Index Admission Gate (memory pressure)
Heavy background index jobs (refresh_symbol_index, index_embeddings) defer
while macOS reports memory pressure at warning level or above
(kern.memorystatus_vm_pressure_level ≥ 2), polling every 2s with fresh job
heartbeats and honoring cancellation between polls. Non-macOS targets and
probe failures read as normal pressure (fail-open).
CODELENS_INDEX_PRESSURE_MAX_DEFER_SECS(u64 seconds, default120) — defer budget per job. After it elapses the job proceeds under pressure (with atracingwarning) so admission gating can never starve a job.0disables deferral entirely (useful for CI or benchmark runs).
Index Exclusions (.codelens/config.json)
The indexer skips a hardcoded directory set (EXCLUDED_DIRS in
crates/codelens-engine/src/project/exclusions.rs): VCS and editor state,
node_modules, target, dist, build, out, generated, vendor,
__pycache__, virtualenvs, caches, git worktrees, and framework build output
(.next, .vercel, .turbo, .svelte-kit, .nuxt, .astro,
.parcel-cache).
The indexer honors .gitignore (since 2026-10-10): the repository's
.gitignore files at every level, .git/info/exclude, and ignore files in
parent directories of a project rooted inside a larger checkout. It does so
only inside a git checkout (a .git directory or a linked worktree's .git
file), like git itself, and it leaves out the user's global excludes file so
the index does not depend on per-machine git config. Hidden directories stay
indexed unless excluded by name. The file watcher applies the same rule, so a
build that rewrites ignored output does not re-add it.
Before this, 16.9% of indexed files across 23 local git projects were
gitignored (one repository 64%: archived production snapshots and a build-output
public/), and every copy re-declared the same symbols, so path-less reference
lookups gave up on ambiguity. Files a repository force-adds despite an ignore
pattern are skipped too; list such paths in the repository's .gitignore with
a ! negation if they must be indexed.
Linked git worktrees kept inside the project (a directory whose .git file points into another checkout's .git/worktrees/, such as .codex-worktrees/<name>) are skipped whatever the ignore rules say: on one repository they were 3,354 of 5,124 indexed files. Submodules (.git file pointing into .git/modules/) stay indexed, and a project bound at a worktree root is walked normally.
CODELENS_INDEX_GITIGNORE=0 restores the previous name-list-only walk (nested worktrees are still skipped). The
rule in effect is recorded in the index (meta.discovery_signature); when it
differs at bind time, one full refresh runs and removes rows for files the
current rule no longer admits. Unchanged files are not re-parsed.
Exclude anything else per project with .codelens/config.json. All three keys are read
and merged, so use whichever reads best:
{
"index": {
"exclude_paths": [".vercel/output", "coverage"],
"exclude": ["**/*.generated.ts"]
},
"exclude_paths": ["fixtures/large"]
}
Pattern rules, from expand_exclude_pattern:
- A plain path expands to both itself and
<path>/**, so".vercel/output"excludes the directory and everything under it. - A pattern already containing
*,?,[, or{, or ending in/, is used verbatim with no expansion. - A leading
./is stripped and\is normalised to/. - Any pattern containing
..is dropped, so exclusions cannot escape the project root.
Changing this file requires a reindex to take effect
(refresh_symbol_index, or analyze with mode=start).
Backup Rotation
Three backup patterns accumulate without retention if left unmanaged:
${REPO}/.codelens/bin/codelens-mcp-http.bak-pre-*— daemon redeploy preserves the previous binary by version tag.~/.codelens/index/{symbols,embeddings}.db.bak-*-migration— in-place schema migrations preserve the previous shape.~/.codelens/index/{symbols,embeddings}.db.bak-readonly-old— read-only conversion preserves the writable copy.
Run bash scripts/cleanup-stale-backups.sh [--keep N] [--dry-run] periodically (or wire into a build/release hook). Defaults to keeping the 2 most recent per pattern.