26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
383 lines
18 KiB
Markdown
383 lines
18 KiB
Markdown
# Journey 13 — Security & Governance
|
|
|
|
> You're putting lean-ctx in front of real code — possibly on a shared machine,
|
|
> in CI, or under a security policy. This journey covers every guardrail lean-ctx
|
|
> applies by default and every dial you can tighten: filesystem jail, shell
|
|
> allowlist, secret redaction, OS sandboxing, harden mode, and role policies.
|
|
|
|
Source files:
|
|
- `rust/src/core/pathjail.rs` — filesystem boundary (PathJail)
|
|
- `rust/src/core/shell_allowlist.rs` — command allowlist
|
|
- `rust/src/core/secret_detection.rs` — secret scanning & redaction
|
|
- `rust/src/core/sandbox.rs`, `sandbox_seatbelt.rs`, `sandbox_landlock.rs` — OS sandbox
|
|
- `rust/src/core/tcc_guard_sandbox.rs` — macOS launchd seatbelt wrapper (deny `~/Documents`, #356)
|
|
- `rust/src/cli/harden.rs` — `harden` (soft/hard/undo)
|
|
- `rust/src/core/context_policies.rs` — role policies
|
|
- `rust/src/core/owasp_alignment.rs`, `audit_trail.rs` — alignment & audit
|
|
|
|
---
|
|
|
|
## 0. The defense-in-depth model
|
|
|
|
Every file read and shell command flows through layered guardrails, **on by
|
|
default** — you don't opt in:
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
R[ctx_read / ctx_shell] --> PJ[PathJail<br/>stay in project root]
|
|
PJ --> AL[Shell allowlist<br/>~200 known binaries]
|
|
AL --> SB[OS sandbox<br/>Seatbelt / Landlock]
|
|
SB --> SD[Secret redaction<br/>before output leaves]
|
|
SD --> OUT[result to the agent]
|
|
```
|
|
|
|
You can tighten each layer (stricter shell parsing, harden mode, role policies)
|
|
but you cannot accidentally turn off the baseline.
|
|
|
|
---
|
|
|
|
## 0.1 See and flip the whole posture — `lean-ctx security`
|
|
|
|
lean-ctx's guardrails fall into **two independent planes**. Keeping them separate
|
|
is deliberate: a usability-first user can let the agent do anything on a trusted
|
|
machine **without** ever leaking secrets to the model provider.
|
|
|
|
| Plane | Protects | Controls |
|
|
|-------|----------|----------|
|
|
| **Containment** | your *machine* from the agent | PathJail + shell gating |
|
|
| **Secret defense** | your *secrets* from the LLM provider | `.env` / credential redaction |
|
|
|
|
One command shows the live posture and how to change it:
|
|
|
|
```bash
|
|
lean-ctx security status # posture board: jail, shell, secret redaction
|
|
```
|
|
|
|
Two master switches flip **containment** in one step — secret redaction is never
|
|
touched by them:
|
|
|
|
```bash
|
|
lean-ctx yolo # OPEN: any path + any command (containment off)
|
|
lean-ctx secure # STRICT: restore the secure defaults
|
|
```
|
|
|
|
`yolo` writes `path_jail = false` and `shell_security = "off"` to your global
|
|
config and takes effect immediately (no restart). It is fully reversible, and you
|
|
re-enable pieces **granularly** afterwards — the array/blanket dichotomy works in
|
|
both directions:
|
|
|
|
```bash
|
|
lean-ctx config set shell_security warn # log violations, block nothing
|
|
lean-ctx config set path_jail true # re-enable the filesystem jail
|
|
lean-ctx allow acli # permit one extra command (additive)
|
|
```
|
|
|
|
Secret/`.env` redaction is a **separate** toggle — `yolo` leaves it on:
|
|
|
|
```bash
|
|
lean-ctx security secrets off # stop masking secrets (NOT recommended)
|
|
lean-ctx security secrets on # re-enable masking
|
|
```
|
|
|
|
`lean-ctx security status` (and `lean-ctx doctor`) print a coarse label —
|
|
**STRICT** (both planes enforced), **RELAXED** (partially loosened), or **OPEN**
|
|
(containment fully off) — so the active posture can never hide.
|
|
|
|
> Mental model: **containment** is "what the agent may touch on this machine";
|
|
> **secret defense** is "what may leave this machine for the provider". `yolo`
|
|
> only drops the first.
|
|
|
|
---
|
|
|
|
## 1. PathJail — stay inside the project
|
|
|
|
**What it does:** confines file access to the resolved project root. Absolute
|
|
paths outside the jail are rejected, so a stray `read /etc/passwd` or a
|
|
path-traversal `../../` cannot escape the workspace.
|
|
|
|
- Re-rooting to a different project root is **off by default**
|
|
(`allow_auto_reroot = false`) — lean-ctx will not silently follow an absolute
|
|
path into another tree.
|
|
- Multi-root setups (`lean-ctx serve --root a:A --root b:B`) jail each root
|
|
independently (`server/multi_path.rs`).
|
|
- **Widen it (array):** add trusted roots with `allow_paths` / `extra_roots`
|
|
(or `LEAN_CTX_ALLOW_PATH`) — reads/writes resolve under those prefixes too.
|
|
- **Disable it (blanket):** set `path_jail = false` to allow *any* path — the
|
|
blanket equivalent of `allow_paths = ["/"]`, for containers/sandboxes where the
|
|
boundary is already external. `lean-ctx yolo` sets this for you, and
|
|
`lean-ctx doctor` flags it loudly while it is active.
|
|
|
|
This is the foundation every other layer assumes.
|
|
|
|
---
|
|
|
|
## 1.5 Workspace trust — gate untrusted project-local config
|
|
|
|
**What it does:** a cloned repo ships its own `.lean-ctx.toml`, which is merged
|
|
over your global config. That merge can raise **security-sensitive** settings —
|
|
replace the `shell_allowlist`, widen the jail via `allow_paths` / `extra_roots`,
|
|
repoint a `proxy.*_upstream`, define `custom_aliases`, or flip
|
|
`rules_scope` / `rules_injection`. lean-ctx treats those as *trusted-only*.
|
|
|
|
For a workspace you have **not** trusted, the sensitive overrides are **withheld**
|
|
(comfort knobs like `compression_level` / `theme` still apply) and a `[SECURITY]`
|
|
warning is logged. Grant trust explicitly:
|
|
|
|
```bash
|
|
lean-ctx trust # trust the current project root
|
|
lean-ctx trust status # show trust state + which overrides are gated
|
|
lean-ctx trust --list # list all trusted workspaces
|
|
lean-ctx untrust # revoke trust for the current root
|
|
```
|
|
|
|
Trust is pinned to **both** the workspace path **and** a content hash of
|
|
`.lean-ctx.toml` — editing the file after trust re-gates it, so a "trust once,
|
|
modify later" change can't take effect silently. `lean-ctx doctor` shows the
|
|
state. Headless / fleet environments can opt in without a prompt via
|
|
`LEAN_CTX_TRUST_WORKSPACE=1` or a `LEAN_CTX_TRUSTED_ROOTS=/path/a,/path/b` list.
|
|
|
|
> Review a clone's `.lean-ctx.toml` before you `lean-ctx trust` it — trusting is
|
|
> what lets its security-sensitive settings widen lean-ctx's own boundaries.
|
|
|
|
---
|
|
|
|
## 2. Shell allowlist & strict mode
|
|
|
|
**What it does:** the shell hook only compresses/executes commands whose binary
|
|
is on the allowlist (~200 common dev tools: `git`, `cargo`, `npm`, `node`,
|
|
`python`, …). Anything else passes through untouched rather than being wrapped.
|
|
|
|
**Pipelines & chains (`|`, `&&`, `||`, `;`):** a compound command is wrapped as a
|
|
*single* `lean-ctx -c "<whole>"` only when **every** stage is gate-clean — then
|
|
the pipe/chain runs natively inside one profile-free shell and just the *final*
|
|
output is compressed (so `git log | wc -l` counts the raw log, not a digest). If
|
|
any stage is not gate-clean (an interpreter `-c`, a non-allowlisted sink such as
|
|
`kubectl`, …) the **whole** command is left raw for the agent's shell. lean-ctx
|
|
never wraps only part of a pipe (which would compress mid-pipe data the next stage
|
|
still needs) and never relaxes the gate to wrap a tricky sink — the rewrite can
|
|
only ever route commands the same chokepoint would already allow.
|
|
|
|
```toml
|
|
# config.toml
|
|
shell_allowlist = ["git", "cargo", "npm", "…"] # REPLACE the default set
|
|
shell_allowlist_extra = ["just", "task"] # ADD to the default set
|
|
shell_strict_mode = false # set true to block $() and backticks
|
|
excluded_commands = [] # never intercept these
|
|
```
|
|
|
|
- **`docker` / `podman` are deliberately *not* in the default allowlist** —
|
|
mount flags can bypass PathJail. Add them explicitly only if you accept that.
|
|
- `shell_strict_mode = true` blocks command substitution (`$(…)`, backticks) for
|
|
environments that must forbid dynamic command construction.
|
|
- Replace the whole list via `LEAN_CTX_SHELL_ALLOWLIST` (comma-separated), or
|
|
just **add** a few extras with `shell_allowlist_extra`. The
|
|
`lean-ctx allow <cmd>` CLI edits `shell_allowlist_extra` for you, and
|
|
`lean-ctx allow --list` prints the effective allowlist plus any parse errors so
|
|
a typo can never silently drop your overrides.
|
|
|
|
### 2.1 Shell-security mode (`enforce` | `warn` | `off`)
|
|
|
|
One switch governs **all** command gating — the allowlist *and* the hard blocks
|
|
(`eval`/`exec`/`source`, `$()`/backticks at command position, interpreter `-c`).
|
|
It is applied at a single chokepoint, so MCP `ctx_shell` and the CLI
|
|
(`lean-ctx -c` / `-t`) behave identically.
|
|
|
|
```toml
|
|
# config.toml
|
|
shell_security = "enforce" # default — secure
|
|
# shell_security = "warn" # run the checks, log violations via tracing, never block
|
|
# shell_security = "off" # skip command gating entirely — compression stays active
|
|
```
|
|
|
|
| Mode | Allowlist | Hard blocks (`eval`, `$()`, …) | Compression |
|
|
|---|---|---|---|
|
|
| `enforce` (default) | enforced | blocked | on |
|
|
| `warn` | logged only | logged only | on |
|
|
| `off` | skipped | skipped | **on** |
|
|
|
|
- **Resolution:** `LEAN_CTX_SHELL_SECURITY` env → `shell_security` in config →
|
|
default `enforce`. An unknown value falls back to `enforce` (never fails open).
|
|
- **Why `enforce` is the default, even for beginners:** lean-ctx mediates the
|
|
agent's shell, so a weaker default would silently downgrade security for every
|
|
install on upgrade. In practice the default allowlist already covers normal
|
|
workflows (`git`/`cargo`/`npm`/`node`/`python`/…), so beginners rarely hit it —
|
|
the friction is for power users running exotic tools, who opt into `off`.
|
|
- **`off` is the "YOLO" escape hatch.** It disables *command gating* only; it does
|
|
**not** lift the read-only-output doctrine in `ctx_shell` (no `>`/`tee`/heredoc
|
|
file writes — use the native write tool). Compression is unaffected in every mode.
|
|
Set it together with `path_jail = false` in one step via `lean-ctx yolo`
|
|
(and revert both with `lean-ctx secure`).
|
|
- `lean-ctx doctor` surfaces the active mode whenever it is not `enforce`, so a
|
|
relaxed posture can never hide.
|
|
- This supersedes the CLI-only `LEAN_CTX_ALLOWLIST_WARN_ONLY` (which still works
|
|
for backward compatibility); `shell_security` is the canonical, global switch.
|
|
|
|
---
|
|
|
|
## 3. OS sandboxing for executed code
|
|
|
|
**What it does:** when lean-ctx executes code (`ctx_execute`), it runs under the
|
|
OS sandbox — **Seatbelt** on macOS and **Landlock** on Linux — so the executed
|
|
process gets a restricted filesystem/network view, not your full user
|
|
privileges. On platforms without a supported sandbox, execution is gated rather
|
|
than run unconfined.
|
|
|
|
This is separate from PathJail (which guards lean-ctx's *own* reads); the sandbox
|
|
guards *child processes* lean-ctx spawns on your behalf.
|
|
|
|
### 3.1 launchd seatbelt — no macOS "Documents" prompt (#356)
|
|
|
|
On macOS the daemon, proxy and auto-updater run as LaunchAgents — i.e. as their
|
|
own TCC identity (`ppid 1`), which does **not** inherit your terminal's or
|
|
editor's privacy grants. Any access they make under `~/Documents`, `~/Desktop`
|
|
or `~/Downloads` would pop the "lean-ctx would like to access files in your
|
|
Documents folder" prompt, and because every release re-signs the binary a
|
|
granted permission is voided on the next update — so the prompt returns forever.
|
|
|
|
lean-ctx therefore bakes a **deny-`~/Documents` Seatbelt profile** into the
|
|
LaunchAgent invocation itself (`rust/src/core/tcc_guard_sandbox.rs`): the plist
|
|
runs `sandbox-exec -p '(allow default) (deny file-read* file-write* …)' lean-ctx
|
|
…`. The kernel refuses any access to the three protected directories *silently*
|
|
with `EPERM` — the TCC subsystem is never consulted, so the prompt can never
|
|
appear and no "Allow" is ever required. Everything outside those directories
|
|
stays permitted, so the processes keep full functionality. The path guards
|
|
(`pathutil::may_probe_path`) and the optional stable code-signing identity
|
|
(`lean-ctx codesign-setup`) remain underneath as defense-in-depth. Verified by
|
|
`rust/tests/tcc_sandbox.sh`, which boots the daemon under the production wrapper.
|
|
|
|
---
|
|
|
|
## 4. Secret redaction — nothing leaks to the model
|
|
|
|
**What it does:** before any shell output or file content is returned, lean-ctx
|
|
scans it for credentials (AWS keys, tokens, etc.) and replaces matches with
|
|
`[REDACTED:<kind>]`. It is **on by default**.
|
|
|
|
```toml
|
|
[secret_detection]
|
|
enabled = true
|
|
redact = true
|
|
custom_patterns = ["MYCORP_[A-Z0-9]{20}"] # add org-specific secret shapes
|
|
exclude_patterns = ["LCTX_PUBLIC_\\w+"] # carve out known-safe matches (subtractive)
|
|
```
|
|
|
|
Built-in patterns cover common cloud/credential formats; `custom_patterns` lets
|
|
you redact organization-specific secret shapes. Matches are reported with a safe
|
|
preview (e.g. `AKIA…`) so you know redaction fired without seeing the secret.
|
|
|
|
`exclude_patterns` is the subtractive counterpart (#718): a detected match
|
|
covered by any of these regexes is neither reported nor redacted — carve out a
|
|
repo's own naming conventions without disabling secret detection wholesale.
|
|
Since v3.9.2 the detector also skips benign non-values on its own: unquoted
|
|
identifier/property references (`serverEnv.getStripeSecretKey`), camelCase
|
|
subwords (`superuserPassword:` as a key), and obvious placeholders
|
|
(`ghp_change_me`, `your_key_here`). Quoted string literals and digit-bearing
|
|
values remain protected.
|
|
|
|
### 4.1 Sensitivity policy floor — per-item levels
|
|
|
|
Where `[secret_detection]` masks *known credential shapes*, the **sensitivity
|
|
floor** classifies every item by a level and enforces one uniform **policy
|
|
floor** just before content reaches the model. It is **off by default** (fully
|
|
no-op) and covers both tool outputs and injected knowledge facts.
|
|
|
|
```toml
|
|
[sensitivity]
|
|
enabled = true # off by default → no-op
|
|
policy_floor = "secret" # public < internal < confidential < secret
|
|
action = "redact" # "redact" masks the spans, "drop" withholds the item
|
|
```
|
|
|
|
| Level | Raised by (high-precision signals only) |
|
|
|-------|------------------------------------------|
|
|
| `secret` | secret-like paths (`.env`, `.ssh/…`, `*.pem`) or detected credentials |
|
|
| `confidential` | Luhn-validated card numbers, mod-97-validated IBANs |
|
|
| `internal` | reserved for explicit tagging |
|
|
| `public` | default |
|
|
|
|
Anything classified **at or above** `policy_floor` is dropped or redacted: with
|
|
`action = "drop"` the whole item is replaced by a short notice; with `redact`
|
|
only the offending spans are masked. Knowledge facts store their level at
|
|
creation (`KnowledgeFact.sensitivity`) and are re-checked at injection time, so a
|
|
floor change takes effect immediately. The classifier uses **only high-precision
|
|
signals** (no speculative heuristics) to avoid false positives; `LEAN_CTX_SENSITIVITY=0|1`
|
|
toggles enforcement for a single run. This section lives in the **global**
|
|
`~/.lean-ctx/config.toml` only — an untrusted project file cannot lower the floor.
|
|
|
|
---
|
|
|
|
## 5. Harden mode — force the compressed path
|
|
|
|
**What it does:** `harden` makes the agent use lean-ctx's compressed `ctx_*`
|
|
tools by **denying native Read/Grep** (except immediately after an Edit). Two
|
|
levels:
|
|
|
|
```bash
|
|
lean-ctx harden # soft: sets LEAN_CTX_HARDEN=1 in MCP configs
|
|
lean-ctx harden --hard # also adds "Bash" to ~/.claude/settings.json deny
|
|
lean-ctx harden --undo # remove both — native tools allowed again
|
|
```
|
|
|
|
Real output:
|
|
|
|
```text
|
|
lean-ctx harden (level: soft)
|
|
|
|
[OK] /Users/you/.cursor/mcp.json
|
|
[OK] /Users/you/.claude.json
|
|
[OK] Set LEAN_CTX_HARDEN=1 in MCP configs
|
|
|
|
Harden active. Native Read/Grep will be denied (except after Edit).
|
|
Undo with: lean-ctx harden --undo
|
|
```
|
|
|
|
Soft harden is reversible and config-only; `--hard` additionally blocks the
|
|
Claude Bash tool. Both are fully undone by `--undo` (see also
|
|
[Journey 12 → §4](12-troubleshooting.md)).
|
|
|
|
---
|
|
|
|
## 6. Role policies — least privilege per agent
|
|
|
|
**What it does:** role policies (`context_policies.rs`, set via
|
|
`ctx_session action=role` / `lean-ctx session role`) scope what a session may do
|
|
— e.g. a `reviewer` role that cannot write, or limiting privileged search
|
|
(`ctx_search ignore_gitignore=true` requires an admin-class role). Use this to
|
|
give a sub-agent or teammate a least-privilege surface.
|
|
|
|
---
|
|
|
|
## 7. Audit & alignment
|
|
|
|
lean-ctx maintains an **audit trail** (`audit_trail.rs`) of security-relevant
|
|
actions and ships an **OWASP alignment** map (`owasp_alignment.rs`) documenting
|
|
which controls address which risks — useful when answering a security review.
|
|
|
|
For a structured external yardstick, lean-ctx publishes a self-assessment
|
|
against the 32-control [Context Governance Benchmark](https://github.com/yvgude/context-governance-benchmark)
|
|
(v1.0-draft): graded **C2 — Managed** with declared gaps — see
|
|
[docs/compliance/cgb-self-assessment.md](../compliance/cgb-self-assessment.md).
|
|
|
|
---
|
|
|
|
## Governance checklist
|
|
|
|
| Goal | Control |
|
|
|------|---------|
|
|
| See / flip the whole posture | `lean-ctx security status` · `yolo` · `secure` |
|
|
| Disable containment for a trusted machine | `lean-ctx yolo` (keeps secret redaction on) |
|
|
| Keep file access in-project | PathJail (on by default) |
|
|
| Gate a cloned repo's `.lean-ctx.toml` | Workspace trust (`lean-ctx trust`) |
|
|
| Restrict which commands run | `shell_allowlist` + `shell_strict_mode` |
|
|
| Never wrap docker mount-escapes | docker/podman off the default allowlist |
|
|
| Confine executed code | Seatbelt (macOS) / Landlock (Linux) |
|
|
| Stop secrets reaching the model | `[secret_detection]` (on by default) |
|
|
| Block whole sensitivity levels (PII/secret) pre-prompt | `[sensitivity]` policy floor (off by default) |
|
|
| Force compressed reads | `lean-ctx harden [--hard]` |
|
|
| Least-privilege agents | role policies |
|
|
| Answer a security review | audit trail + OWASP alignment |
|
|
|
|
> Tuning *how much* lean-ctx compresses or *which tools* it exposes lives in
|
|
> [Journey 10 — Customization & Governance](10-customization-and-governance.md);
|
|
> this journey is specifically the **security** surface.
|