# Security Policy
## Reporting a Vulnerability
If you discover a security vulnerability in lean-ctx, please report it privately:
- **Email**: hello@thinkery.ch
- **GitHub**: [Create a private security advisory](https://github.com/yvgude/lean-ctx/security/advisories/new)
- **Response time**: We aim to acknowledge reports within 48 hours
- **Disclosure**: We follow responsible disclosure practices (90-day embargo)
**Please do NOT:**
- Open public GitHub issues for security vulnerabilities
- Disclose vulnerabilities on social media or forums before we've had a chance to address them
---
## What lean-ctx Does (and Doesn't Do)
LeanCTX is the **Context Gateway for AI Systems**. **Control what your AI can see.**
This policy covers the open-source **LeanCTX Engine** and its local CLI, MCP,
HTTP, hook, and supported proxy paths. **LeanCTX SDK** and commercial Enterprise
deployments have separate integration and operating contracts.
The Engine runs locally, but enabled providers, upstream model traffic, updates,
telemetry, and other configured integrations can make network requests. Review
the configured integration and deployment rather than infer data locality from
the word “local.”
Context selection determines what AI should see; permissions and configured
content controls constrain what it may see. Receipts describe observed context
operations, not prompts or delivery outside the integration's visibility.
**Does:**
- Read files from your local filesystem (explicit reads and tool-driven scans within the project boundary)
- Execute shell commands (only commands you or your AI tool explicitly invoke)
- Cache file contents in memory during a session
- Store statistics in `~/.lean-ctx/stats.json` (command counts, token savings)
- Store session state in `~/.lean-ctx/sessions/` (task context, findings)
### I/O Boundary (PathJail + Roles)
lean-ctx enforces a **filesystem boundary** for tool I/O:
- **PathJail**: all tool path inputs are resolved (symlinks and `..` included) and checked
against the jail scope (`path_jail_scope`, global-only):
- `home` (default since v3.11.1; read **and write** below `~` since v3.11.3): every path
below the user's home directory, except the protected zones — every top-level dot entry (`~/.ssh`, `~/.aws`, `~/.gnupg`,
`~/.config`, shell rc files, other agents' `~/.claude` / `~/.codex` / `~/.cursor`),
`~/Library`, `~/AppData` / `NTUSER.DAT*` and `~/snap`. Everything outside `~` stays jailed.
Zones are a hard deny: a root or allow entry that merely contains a zone does not open it.
An implausible `$HOME` (`/`, `/root`, `/tmp`, not owned by the user) disables the scope.
- `projects` (the default of v3.11.1–v3.11.2): only paths inside a project below `~` (a folder
between it and `~` holds `.git`, `Cargo.toml`, `package.json` or another project marker) are
readable; loose personal files stay jailed, and writes stay limited to the session's project,
host-declared roots and explicit allow entries.
- `project`: only the current `project_root` plus the explicit allow roots below.
- If a path would escape, the call fails naming the single command that admits it
(`lean-ctx allow-path
`) and asks the agent to defer to the user.
- **Per-session project binding**: the project root comes from the host session (MCP roots,
host project env, working directory). Global agent configs no longer carry a
`LEAN_CTX_PROJECT_ROOT` pin; `lean-ctx doctor` reports and `doctor --fix` removes old ones.
- **Explicit allow roots** (`lean-ctx allow-path ` writes `allow_paths`):
- Env: `LEAN_CTX_ALLOW_PATH` (or `LCTX_ALLOW_PATH`) — a path list (`:` on Unix, `;` on Windows)
- Config: `allow_paths` in `~/.lean-ctx/config.toml` (whitelist only); `extra_roots` (whitelist + multi-root scanning)
- `~`, `$VAR` and `${VAR}` are expanded in these entries (no shell runs for config files)
- **Disabling the jail** (sandboxed environments where the OS is the boundary):
- Config: `path_jail = false` in `~/.lean-ctx/config.toml`
- Compile-time: the `no-jail` cargo feature
- The legacy `LEAN_CTX_NO_JAIL=1` env var was removed in v3.7.4 and has no effect
- Full reference: `docs/reference/appendix-paths-and-config.md` §5; `lean-ctx doctor` reports the effective jail state
- **Symlink escape protection**: canonicalization ensures that symlinks pointing outside the jail are rejected.
In addition, roles can restrict **unsafe I/O**:
- **Secret-like deny-by-default**:
- Search skips secret-like files (e.g. `.env`, `*.pem`, `id_rsa`, `.ssh/`, `.aws/`) unless the active role explicitly allows them.
- Artifact registry resolution rejects secret-like artifact paths unless allowed (artifacts are indexed/shareable by design).
- Direct reads/edits can warn or error depending on boundary mode.
- **`.gitignore` bypass is policy-gated**:
- `ctx_search ignore_gitignore=true` requires explicit role permission (typically the `admin` role).
- **Boundary mode**:
- Roles can set `io.boundary_mode = "warn" | "enforce"`.
- Env override: `LEAN_CTX_IO_BOUNDARY_MODE=warn|enforce`.
- **Auditability**:
- Boundary denials/warnings emit local `PolicyViolation` events (no secret content is returned as part of the violation).
### Threat Model (v2)
**Primary risks:**
- **Accidental secret exfiltration to LLMs** via `ctx_read`, `ctx_search`, compressed `ctx_shell`, archives, or exported artifacts.
- **Boundary escapes** via absolute paths, symlinks, linked projects, or artifact path tricks.
- **Amplification / token burn** by scanning large files or returning unbounded outputs.
- **ReDoS** via user-supplied regex patterns in `ctx_search`.
- **Cross-workspace data access** in team server deployments (IDOR).
**Core mitigations:**
- **PathJail** + explicit allow roots (`LEAN_CTX_ALLOW_PATH` / `allow_paths`).
- **Role-gated unsafe I/O** (`ignore_gitignore`, secret-like allow).
- **Secret-path handling** — search skips recognized secret-like paths; direct-read warnings or rejection depend on the configured I/O boundary, permissions, and role.
- **Shell CWD jail enforcement** — explicit `cwd` parameters are jail-checked, `cd` targets validated.
- **Deterministic redaction** on tool outputs (non-admin roles, and for persisted archives).
- **Hard caps** on reads and outputs to limit DoS/token burn.
- **Regex guards** — pattern length (1024 chars) and DFA size (1 MiB) limits on `ctx_search`.
- **MCP message size limit** — 32 MiB cap on JSON-RPC message size.
- **Constant-time token comparison** in all auth paths (dashboard, HTTP server, team server).
- **Team server tenant isolation** — workspace enforced from authenticated token, not query parameters.
- **JSON-RPC batch rejection** — batch requests rejected on team server to prevent scope bypass.
- **Event payload redaction** — REST API responses redacted to `Summary` level by default.
- **Pipeline archive redaction** — shell output archives redacted before storage.
- **UDS socket permissions** — `0o600` enforced on Unix domain sockets after bind.
- **Error response sanitization** — internal details logged server-side, generic codes returned to clients.
**Optional network activity (review effective configuration):**
- **Update check**: a lightweight daily GET to `leanctx.com/version.txt` to notify you of new versions. Sends only the current version as User-Agent. Disable with `update_check_disabled = true` in `~/.lean-ctx/config.toml` or `LEAN_CTX_NO_UPDATE_CHECK=1`.
- **Telemetry**: product telemetry is on by default since v3.11.0 (v3.10.5 and earlier defaulted it off); an explicit earlier opt-out is kept. Inspect your installed build with `lean-ctx telemetry status` and `lean-ctx telemetry show`. Disable sending with `lean-ctx telemetry off`, `DO_NOT_TRACK=1`, or `LEAN_CTX_TELEMETRY=off`. Enabled telemetry sends installation and aggregate usage metadata. Do not infer zero network activity from telemetry being disabled.
- **Configured providers and integrations**: source connectors, upstream model requests, submitted feedback, and remote endpoints have their own payloads and credentials. Review each enabled path.
**Limits:**
- Configured detectors recognize supported patterns; they do not detect every secret or sensitive datum.
- Warning, redaction, and blocking are different actions. Roles, bypasses, allow roots, and disabled controls change the effective boundary.
- Inputs, caches, archives, and integration credentials require appropriate host protection and retention settings.
- The normal Engine runs as your user; a container, VM, or operating-system policy can supply an additional boundary.
---
## Automated Security Checks
Every push and pull request triggers our CI security pipeline:
1. **`cargo audit`** — Scans dependencies for known CVEs
2. **`cargo clippy`** — Enforces Rust safety lints (warnings = errors)
3. **Dangerous pattern scan** — Detects potentially unsafe code patterns:
- Shell injection vectors (`Command::new("sh")` with user input)
- Network operations (`reqwest::`, `std::net::`, `hyper::`)
- Unsafe code blocks (`unsafe {`)
- Environment manipulation (`.env("LD_PRELOAD")`)
- Hardcoded secrets or obfuscated strings
4. **`cargo test`** — Full test suite must pass
---
## Critical Files Requiring Enhanced Review
Changes to these files receive extra scrutiny:
| File | Risk | Why |
|------|------|-----|
| `rust/src/shell/` | Shell execution | Wraps your shell, executes commands |
| `rust/src/server/` | MCP protocol | Handles all tool calls from AI editors/agents |
| `rust/src/hooks/` | Editor integration | Installs hooks/config into Claude Code, CodeBuddy, Cursor, etc. |
| `rust/src/core/cache.rs` | File caching | Reads and stores file contents |
| `rust/Cargo.toml` | Supply chain | Dependency manifest |
| `.github/workflows/*.yml` | CI/CD | Release pipeline integrity |
---
## Dependency Security
The manifests and lockfile define the dependency set for a given release.
Review `rust/Cargo.toml`, `rust/Cargo.lock`, enabled features, and the security
workflow when assessing supply-chain exposure. Dependency counts, download
counts, and recent commits alone are not evidence that a dependency is safe.
---
## VirusTotal False Positives
Rust binaries are frequently flagged by ML-based antivirus engines (particularly Microsoft Defender's `Wacatac.B!ml` classifier). This is a **known issue** affecting many Rust projects:
- [Rust lang discussion on false positives](https://users.rust-lang.org/t/rust-programs-flagged-as-malware/49799)
- A low number of detections alone does not establish a false positive.
- The `!ml` suffix in `Wacatac.B!ml` means "Machine Learning detection" (heuristic, not signature-based)
**Why it happens:**
- Statically linked binaries (~30 MB) are unusual for Windows
- `strip = true` + `lto = true` optimizations alter binary structure
- New/unsigned executables trigger ML classifiers trained on known-good signed software
**How to verify lean-ctx yourself:**
1. Build from source: `cargo install lean-ctx` (compiles on your machine)
2. Compare SHA256 checksums against our [GitHub Releases](https://github.com/yvgude/lean-ctx/releases)
3. Audit the source code: the entire codebase is open source
---
## Build Reproducibility
To build the source and inspect its reported version:
```bash
# Clone and build
git clone https://github.com/yvgude/lean-ctx.git
cd lean-ctx/rust
cargo build --release
# Compare with installed version
lean-ctx --version
./target/release/lean-ctx --version
```
Use the checksums attached to the relevant [GitHub Release](https://github.com/yvgude/lean-ctx/releases) to check a downloaded artifact. Matching version strings does not prove byte-for-byte reproducibility or establish that a binary matches source.
---
## Disclosure Timeline
When vulnerabilities are reported:
1. **Day 0**: Acknowledgment sent to reporter
2. **Day 7**: Severity assessment completed
3. **Day 14**: Patch development begins
4. **Day 30**: Patch released + CVE filed (if applicable)
5. **Day 90**: Public disclosure
Critical vulnerabilities (RCE, data exfiltration) are fast-tracked.
---
## Known Residual Risks
### TOCTOU (Time-of-Check to Time-of-Use)
**Status:** Mitigated on Unix; residual risk on Windows.
A race window exists between `jail_path` validation and the subsequent file operation. Mitigations in place: standard reads open with `O_NOFOLLOW` (Unix) and reject symlinks; `ctx_edit` additionally rejects symlinks on both its read and write paths (lstat pre-check on all platforms, `O_NOFOLLOW` on Unix) and re-verifies the file fingerprint (size/mtime/md5) immediately before writing. On Windows there is no `O_NOFOLLOW` equivalent, so the lstat pre-check is the only guard — it rejects symlinks **and all NTFS reparse points (junctions, mount points)** via `pathutil::is_symlink_or_reparse`, and `read_file_nofollow` applies the same lstat check before opening. The residual risk is the unavoidable check→open race window.
**Recommendation for regulated environments:** Run lean-ctx inside a container or VM where the filesystem is controlled and no untrusted processes can modify symlinks concurrently.
### ctx_execute Sandbox Naming
**Status:** Documented limitation.
The `ctx_execute` tool provides timeout enforcement and output capping.
OS isolation depends on `sandbox_level`, platform support, and the selected
execution path. Configured level 1 uses Seatbelt on macOS or Landlock on Linux;
unsupported platforms can fall back to level 0. Do not treat the tool name as
proof of OS isolation. Inspect `rust/src/core/sandbox.rs` for the release in use.
**Recommendation for regulated environments:** Disable `ctx_execute` via role configuration (`denied: ["ctx_execute"]`) or run lean-ctx in a pre-existing container sandbox.
### Shell Command Validation (REQ-57177, GH #391)
**Status:** Defense in depth — the agent's permission model remains the primary boundary. **`ctx_shell` is not a sandbox.**
`ctx_shell` and `lean-ctx -c` enforce, in both allowlist and blocklist-only mode:
- a deny-by-default executable allowlist when configured (AST-segmented: every segment of a pipeline/compound command must be allowed),
- `eval`/`exec`/`source` unconditionally blocked; `$()`/backticks blocked at command position,
- **interpreter inline-code blocking**: `bash -c`, `sh -c`, `python -c`, `node -e`, … are rejected (including via delegation wrappers like `env`, `timeout`, `xargs`) — quoting a payload inside `bash -c '…'` does not bypass the file-write or allowlist checks because the interpreter call itself is refused,
- file-write detection (`>`, `>>`, `tee`, heredoc-to-file, `dd of=`, `curl -o/-O`, `wget` to file) — output capture (`>`, `>>`, `tee`) may go to the session's project, `allow_paths` / `extra_roots`, `write_allow_paths` or a scratch path (never `/`, `~`, an ancestor of `~` or a `read_only_roots` entry); downloads only to scratch paths and `write_allow_paths`; everything else belongs to the editor's native Write/Edit tools where the agent's permission UI governs them,
- dangerous-flag blocking (`git --upload-pack`, `tar --to-command`, `find -exec`, `awk system()`), inline env hijack blocking (`PATH=`, `LD_PRELOAD=`, `GIT_SSH_COMMAND=`, …), and dangerous env-key filtering on the MCP `env` parameter,
- **no self-loosening** (allowlist mode, the default): lean-ctx subcommands that widen its own guardrails — `yolo`, `allow `, `allow-path `, `trust`, `security open`, `security secrets off`, `config set` of a security or path/allowlist key — are refused, so an agent cannot undo a refusal it just received. Listing, `--remove`, `secure`, `untrust` and `security secrets on` still run. The user runs the blocked commands in their own terminal (warn-only there, see below). `ctx_execute` is not covered — see the next paragraph.
Enforcement applies to the MCP path, hook-child mode and every non-interactive CLI invocation; an interactive human terminal gets a warning instead (`LEAN_CTX_ALLOWLIST_WARN_ONLY=1` opts out explicitly). Cloud/infra mutation CLIs (terraform, kubectl, aws, …) are excluded from the default allowlist and require per-tool opt-in (`lean-ctx allow `). `shell_strict_mode = true` upgrades the warn-only heuristics (command substitution in arguments, pipe-to-bare-interpreter) to hard blocks.
**`ctx_execute(language=shell)` is deliberately NOT allowlisted (GH #905).** The checks above gate `ctx_shell` and `lean-ctx -c` only. `ctx_execute` is the trusted script path: calling it is an explicit decision to write and run a script, the same decision a native Write plus `bash script.sh` would express, so re-running the allowlist there would only tax the sanctioned route. The consequence must be stated plainly rather than assumed: **the two tools are one trust tier, not a boundary and a bypass.** Any agent that can call `ctx_execute` has full shell reach no matter how `shell_allowlist` is configured — tightening the allowlist does not constrain it. Where that reach is unacceptable, deny the tool by role (`denied: ["ctx_execute"]`) or run the stack in an OS sandbox; those are the only controls that bind it.
**Explicitly out of scope:** commands run with the invoking user's full privileges. Anything the user can do, an allowed command can do — `cp`/`rsync` can copy any file the OS lets the user read, package managers execute arbitrary install scripts, `npm test` runs project code. A blocklist cannot enumerate these; pretending otherwise would be security theater.
**Rationale:** Shell filters can be bypassed by a sufficiently creative attacker, so the agent's permission model (Claude Code allowlists, Cursor approval dialogs) remains the primary boundary — lean-ctx's allowlist is a second, independent layer, not a replacement.
**Mitigation for untrusted agents:** Use role-based `denied: ["ctx_shell"]` to disable shell access entirely, enable the deny-by-default allowlist with a minimal command set, and/or run the whole agent stack inside an OS sandbox (container, gVisor/Firecracker, bwrap, seccomp/AppArmor) — that is the correct layer for kernel-grade isolation.
### PathJail TOCTOU Race (REQ-57178)
**Status:** Mitigated on Unix; residual risk on Windows.
A race window exists between `jail_path` validation and the subsequent file operation. Mitigations: symlink-following canonicalization before access, `O_NOFOLLOW` + symlink rejection on read paths (Unix), and symlink rejection on `ctx_edit` write paths (all platforms, lstat-based; on Windows this includes NTFS junctions and other reparse points). Home-level IDE config dirs (`~/.cursor`, `~/.claude`, …) are excluded from the jail's allow-list by default (`allow_ide_config_dirs` opts in).
**Windows file permissions:** the Unix `0o600`/`0o700` tightening (cloud credentials, crash log) has no direct Windows equivalent; protection relies on the default NTFS ACL of the user profile (`%USERPROFILE%` is not readable by other non-admin users). Machines with custom ACLs on the profile directory should verify `%USERPROFILE%\.lean-ctx` inherits owner-only access.
**Mitigation:** For regulated environments: run lean-ctx inside a container where no untrusted processes can modify symlinks concurrently.
### Cloud Server Database TLS (REQ-57188)
**Status:** Accepted risk — localhost-only by default.
The cloud server's PostgreSQL connection does not enforce TLS by default. This is acceptable because the cloud server is designed for localhost/loopback deployment where DB traffic does not traverse a network.
**Mitigation for production:** Set `DATABASE_URL` with `?sslmode=require` or use a connection string that enforces TLS. When deployed behind a reverse proxy (nginx/Caddy), ensure TLS terminates before the DB.
### HuggingFace Model Downloads
**Status:** Documented risk.
Embedding models for semantic search are downloaded from HuggingFace Hub. Verification is size-based heuristic only, not cryptographic (no SHA256 pinning for model files).
**Recommendation for regulated environments:** Pre-provision models manually from an internal mirror with signature verification. Set `LEAN_CTX_EMBEDDING_MODEL_DIR` to point to the pre-provisioned directory to skip downloads entirely.
### Project-Scope Config Influences Injected Context (external audit, finding 4)
**Status:** Mitigated by Workspace Trust (selective gating + content-hash pin).
A cloned repository's `.lean-ctx.toml` is merged over the global config by `Config::merge_local`. That merge can raise **security-sensitive** settings — replace the shell allowlist, widen the path jail (`allow_paths` / `extra_roots`), repoint the proxy upstream, define command aliases, change `rules_scope` / `rules_injection`, or widen the exposed tool surface (`tool_profile` / `tools_enabled` / `default_tool_categories`). Opening an untrusted clone with an agent would let the repo silently weaken lean-ctx's own boundaries.
**Mitigation (shipped):** lean-ctx now applies a VS-Code-style **Workspace Trust** gate. For a workspace the user has not trusted, the security-sensitive overrides above are **withheld** (comfort knobs like `compression`/`theme` still apply) and a `[SECURITY]` warning is logged; `lean-ctx doctor` shows the state. Trust is granted explicitly with `lean-ctx trust` and pinned to BOTH the workspace path AND a content hash of `.lean-ctx.toml`, so editing the file after trust re-gates it (a "trust once, modify later" change cannot take effect silently). Headless/fleet use can opt in via `LEAN_CTX_TRUST_WORKSPACE=1` or `LEAN_CTX_TRUSTED_ROOTS`.
**Residual:** Model-visible *content* lean-ctx injects (the static `` rules block, hook `additionalContext`, `[VERIFY]`/`[HINT]` suffixes) is itself auditable and not repo-controlled. Review a clone's `.lean-ctx.toml` before `lean-ctx trust`.
### LLM Proxy is a Full MITM on API Traffic (external audit, finding 6)
**Status:** By design; loopback-bound and disabled by default.
When enabled, the optional LLM proxy (`lean-ctx proxy`) reads and rewrites every request body (compression, history pruning) and sees `Authorization` headers in cleartext — a concentrated sensitive-data surface. Any process able to reach the port, or a forwarding bug, would expose prompts, completions and API keys.
**Mitigation:** The proxy is disabled by default and binds to loopback (`127.0.0.1`) with an auto-generated auth token when enabled; keep it loopback-bound. The MCP `ctx_*` tools deliver compression savings without routing API traffic through any proxy — leave it disabled unless you need pay-as-you-go key forwarding.
---
## Security Architecture for Enterprise Deployments
### Recommended Configuration (Bank / Regulated)
LeanCTX splits **global settings** (`config.toml`) from **capability policy**
(role files): `config.toml` does *not* define roles or a top-level `[io]`
policy — per-session I/O limits live on the active role, which is selected via
`LEAN_CTX_ROLE`.
**1. Global hardening — `~/.lean-ctx/config.toml`:**
```toml
update_check_disabled = true # no daily update check
path_jail = true # keep the filesystem jail on (default)
path_jail_scope = "project" # only the active project (default "home": all of ~ except protected zones)
[telemetry]
enabled = false # explicitly disable telemetry
preference = "explicitly_disabled"
```
**2. A locked-down role — `~/.lean-ctx/roles/bank.toml`:**
```toml
[role]
name = "bank"
description = "Locked-down role for regulated environments"
[tools]
denied = ["ctx_execute", "ctx_shell"] # no code or shell execution
[io]
boundary_mode = "enforce" # error (not warn) on boundary hits
allow_secret_paths = false # never read .env / keys
allow_ignore_gitignore = false # never scan .gitignore'd paths
allow_cross_project_search = false # stay within this project
```
**3. Activate the role** where lean-ctx (or the MCP server) starts. The active
role resolves in order **env → project `.lean-ctx/roles/` → global
`~/.lean-ctx/roles/` → built-in**:
```bash
export LEAN_CTX_ROLE=bank
```
### Network Surface
| Endpoint | Purpose | Disable |
|----------|---------|---------|
| `leanctx.com/version.txt` | Update check (daily GET) | `update_check_disabled = true` |
| `api.leanctx.com` | Enabled telemetry (version, OS/arch, installation ID, client/setup categories, aggregate tool usage) | `lean-ctx telemetry off` or `DO_NOT_TRACK=1` |
| `huggingface.co` | Embedding model download | Pre-provision models, set `LEAN_CTX_EMBEDDING_MODEL_DIR` |
| `localhost:PORT` | Dashboard (local TCP) | Don't start dashboard, or bind to loopback only |
| UDS socket | Daemon IPC | Permissions `0o600`, owner-only access |
### Team Server Hardening (lean-ctx-enterprise)
The team server is part of `lean-ctx-enterprise` (ADR-023). When deployed:
1. **Token rotation**: Rotate workspace tokens periodically. Tokens are stored in the team config.
2. **Scope minimization**: Grant only necessary scopes per workspace token (e.g., `read` only, no `shell`).
3. **Network isolation**: Bind the team server to an internal network interface, not `0.0.0.0`.
4. **Audit log monitoring**: Team server writes audit logs for all tool calls. Monitor for denied requests.
5. **JSON-RPC batch requests**: Rejected by default to prevent scope bypass.
### Supply Chain
- **`cargo audit`** runs on every CI push (zero known CVEs tolerated).
- **`cargo deny`** checks licenses and advisories.
- **npm `postinstall.js`** verifies SHA256 of downloaded binaries against `SHA256SUMS` release asset.
- **GitHub Actions** uses pinned action versions with hash verification.
---
## Contact
- **Security issues**: hello@thinkery.ch
- **General questions**: [GitHub Discussions](https://github.com/yvgude/lean-ctx/discussions)
- **Discord**: [Join our server](https://discord.gg/6qUJ3QmDyP)
---
**Last updated**: 2026-10-19