Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions docs/concepts/loadouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ baseline packages. This is how you land your editor and tools in the sandbox
without asking the project to depend on them:

```toml
packages = ["helix", "zellij", "ripgrep"]
packages = ["helix", "zellij"]
```

Packages compose as a set across all contributors, so listing something the
Expand All @@ -88,7 +88,6 @@ fallback:
[vars]
EDITOR = "hx"
VISUAL = "hx"
TERM = { inherit = true, default = "xterm-256color" }
```

Vars are the main lever for interactive setup, because the shell you get from
Expand Down Expand Up @@ -118,7 +117,7 @@ that may not be present on every machine.

> **Coming soon.** Lifecycle hooks are not yet live: a hook declared in a
> loadout is accepted, but the current release excludes it from composition
> and nothing executes it. The declaration format below is what will ship.
> and nothing executes it. The declaration format below is what we expect to ship.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align lifecycle-hook status with the reference page.

This says hooks are excluded from composition, but docs/reference/loadouts.md Lines 218-219 says they are composed and recorded with the session but not executed. Document one canonical behavior.

Proposed wording
-> and nothing executes it. The declaration format below is what we expect to ship.
+> but it is not executed. The declaration format below is what we expect to ship.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> and nothing executes it. The declaration format below is what we expect to ship.
> but it is not executed. The declaration format below is what we expect to ship.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/concepts/loadouts.md` at line 120, Update the lifecycle-hook description
in the loadouts concepts documentation to match the canonical behavior in the
reference loadouts documentation: hooks are included in composition and recorded
with the session, but are not executed. Remove or revise the statement that
hooks are excluded from composition, preserving the surrounding
declaration-format guidance.


Hooks are scripts declared to run at session transition points: `on_activate`
when the session comes up, `on_destroy` when it is torn down, and `on_failure`
Expand All @@ -127,7 +126,6 @@ up after a failed start:

```toml
[[lifecycle_hooks]]
description = "warm the grammar cache"
on_activate = { type = "inline", value = "hx --grammar fetch >/dev/null 2>&1 || true" }
```

Expand All @@ -150,7 +148,6 @@ patches = [

[vars]
EDITOR = "hx"
TERM = { inherit = true, default = "xterm-256color" }

[[lifecycle_hooks]]
on_activate = { type = "inline", value = "hx --grammar fetch >/dev/null 2>&1 || true" }
Expand Down
6 changes: 2 additions & 4 deletions docs/reference/loadouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ patches = [
[vars]
EDITOR = "hx"
VISUAL = "hx"
TERM = { inherit = true, default = "xterm-256color" }
COLORTERM = { inherit = true }

# Declared to warm helix's tree-sitter grammar cache when the session
# comes up. Best-effort; failures don't tank activation.
Expand Down Expand Up @@ -128,8 +126,8 @@ Variables set in the session environment. Names must be POSIX-shaped
```toml
[vars]
EDITOR = "hx" # literal value
TERM = { inherit = true, default = "xterm-256color" } # inherit, with fallback
COLORTERM = { inherit = true } # inherit from the host env
PAGER = { inherit = true, default = "less" } # inherit, with fallback
MUXER = { inherit = true } # inherit from the host env
```

- A **literal** string sets the variable to that value.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"minimal-dot-toml.md",
"tasks.md",
"loadouts.md",
"user-policy.md",
"build-specs.md",
"stack-specs.md",
"sandbox-operations.md",
Expand Down
217 changes: 217 additions & 0 deletions docs/reference/user-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
---
title: User policy
description: "The user_policy.toml reference: the vars/patches allow-deny-ignore schema, where the file lives, how min gates loadout and project contributions against it, and how interactive prompts write rules back."
---

# User policy

Your **user policy** constrains which environment variables and file patches
may enter a session. When [`min session activate`](./cli-min.md#session-activate)
composes a session, it gathers the variables and patches contributed by your
[loadouts](./loadouts.md) and the project's
[`minimal.toml`](./minimal-dot-toml.md) and evaluates each against
`user_policy.toml` before the session finalizes. The policy records these
decisions once — for example, that variables named `AWS_*` are always rejected,
or that patches may originate only under `~/.config` — rather than requiring a
choice on every activation. The allow list does not apply to values specified in your
loadouts, only the ignore and deny lists do.

The policy is enforced **only on the client**, inside `min`, never on the
daemon (see [Sessions](../concepts/sessions.md)). It gates two domains:

- **Variables** — matched by variable **name**.
- **Patches** — matched by the **source file paths** a patch enumerates on
the host (the patch's `dest` is never matched).

Packages and lifecycle hooks are not gated by the user policy as they do not request user data.

Packages are effectively out of scope. A package cannot supply file
patches, nor environment variables that carry host data (values inherited from
your shell); Because packages cannot transfer host data into a session, the policy's protective
purpose does not apply to them.
Comment on lines +26 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve the contradictory package-policy semantics.

Lines 26-31 say packages are not gated by user policy, while Lines 163-165 say a package can reach the policy allow step through a static-valued variable. Clarify whether static package variables are gated, then update the surrounding explanation consistently.

Based on the supplied policy reference text.

Also applies to: 163-165

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/user-policy.md` around lines 26 - 31, Resolve the
contradiction between the package exemption described near “Packages and
lifecycle hooks” and the static-valued variable behavior in the package
allow-step section. Explicitly state whether static package variables are
subject to user-policy gating, then revise both explanations consistently while
preserving the documented treatment of packages, file patches, and host-derived
environment variables.


## Where the policy lives

A single TOML file, beside your client [`config.toml`](./loadouts.md#client-config):

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to this fenced code block.

The path-only fence triggers markdownlint MD040. Use text (or another appropriate language) after the opening fence.

Based on the markdownlint-cli2 warning.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 37-37: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/user-policy.md` at line 37, Add a language identifier,
preferably text, to the opening fenced code block in the user policy
documentation while preserving its contents and closing fence.

Source: Linters/SAST tools

<config>/minimal/user_policy.toml
```

`<config>` is the platform user config directory: `$XDG_CONFIG_HOME` on Linux
(or `$HOME/.config` when unset); macOS also uses `$HOME/.config`. The global
[`--config-dir`](./cli-min.md#global-flags) flag overrides the base, and
`min dirs` prints the resolved config directory.

The file is optional. A missing file is treated as an empty policy — a fresh
install activates fine without it. Only `NotFound` is silenced: a file that
exists but can't be read (permissions) or doesn't parse (bad TOML, an invalid
glob) fails the activation loudly.

## Example

```toml
[vars]
allow = ["MY_APP_*", "RUST_*"]
deny = ["AWS_*", "*_TOKEN"]
ignore = ["_*"]

[patches]
allow = ["~/.config/**", "/etc/xdg/**"]
deny = ["~/.ssh/**", "**/*.pem"]
ignore = ["**/.DS_Store"]
```

## Schema

The file has two optional sections, `[vars]` and `[patches]`. Each holds three
optional keys — `allow`, `deny`, and `ignore` — and each key is a list of glob
patterns. Every key defaults to empty, so any section or key may be omitted; an
empty file is a valid empty policy.

Each of the six lists accepts either a single bare string or a list of strings:

```toml
[vars]
deny = "AWS_*" # single pattern
[patches]
deny = ["~/.ssh/**", "**/*.pem"] # list of patterns
```

### `[vars]` — Environment-variable rules

Patterns match against variable **names** and are standard globs (`*`, `?`,
`[...]`, `**`). They are compiled when the file loads, so a malformed glob
fails activation immediately with an `invalid glob` error naming the pattern.

| Key | Matches names that… |
|-----|---------------------|
| `allow` | non-user sources (the project) may set. Loadout-origin vars auto-pass this step — see [Origin semantics](#origin-semantics) |
| `deny` | no source may set, regardless of origin. An unconditional reject |
| `ignore` | are silently dropped without prompting, regardless of origin |

```toml
[vars]
allow = ["MY_APP_*", "RUST_*"]
deny = ["AWS_*", "*_TOKEN"]
ignore = ["_*"]
```

### `[patches]` — File-patch rules

Patterns match against the **source file paths** a patch enumerates on the
host, checked per file after the patch's glob is walked. A patch's `dest`
inside the session is never matched.

| Key | Matches source paths that… |
|-----|----------------------------|
| `allow` | the project may read from. Loadout-origin patches auto-pass this step |
| `deny` | no patch may read from, regardless of origin |
| `ignore` | are silently dropped without prompting, regardless of origin |

Only the project and your loadouts can contribute patches; packages cannot, so
in practice this section arbitrates project patches against your loadouts'.

Patterns are path globs and may use two forms of expansion, resolved against
the session's already-resolved variables at activation:

- A leading `~/` expands to the host home directory.
- `$NAME` / `${NAME}` references (including `$HOME`) resolve against the
session vars; referencing an undefined name fails the activation.

Unlike loadout patch **sources**, a policy pattern need not be absolute — it is
a matcher, not a walk seed, so a bare `**/*.pem` matches any `.pem` path at any
depth. Patch patterns are held verbatim and compiled only at expansion time
(after variable substitution), so a glob that is invalid on its own but valid
once a `$VAR` is substituted is not rejected up front.

Both the resolved target path and, when a patch source traverses a symlink,
the link path are checked independently; the most restrictive outcome wins.

```toml
[patches]
allow = ["~/.config/**", "/etc/xdg/**"]
deny = ["~/.ssh/**", "**/*.pem"]
ignore = ["**/.DS_Store"]
```

## How a contribution is decided

Every variable and every patch source is categorized against the relevant
section in a fixed precedence:

1. **`deny`** — if it matches, the composition fails. Deny takes precedence
over every other rule, including `ignore`: an item matched by both `deny`
and `ignore` resolves as **denied**, so a would-be rejection cannot be
masked by an ignore glob.
2. **`ignore`** — if it matches, the item is silently dropped from the session
(no prompt, no failure).
3. **allow** — origin-aware, described next.

### Origin semantics

Every item carries the source that contributed it, and the allow step depends
on it:

- **User-origin** (items from your own [loadouts](./loadouts.md)) **auto-pass**
the allow step — you don't have to allow-list what you declared yourself.
They are still subject to `deny` and `ignore`.
- **Non-user-origin** items must match an `allow` pattern to pass cleanly. If
none matches, the item is **undecided** and routes to an interactive prompt
(or aborts under `--no-prompt`; see below). In practice the non-user
contributor is the **project**: it can contribute both gated patches and
gated variables. A package can only reach this step with a static-valued
variable — its patches and host-inherited variables are dropped before the
gate.

Composing several loadouts does not compose policy — the policy is a single
file about what you let *other* sources contribute, kept separate from what
your loadouts themselves contribute.

## Prompts and writing rules back

When the policy can't decide a non-user-origin item (no `allow`, `deny`, or
`ignore` match), `min` prompts interactively during activation. Each prompt
offers six choices:

| Choice | Effect |
|--------|--------|
| Allow once | Accept for this activation only |
| Allow permanent | Accept and append the name/path to `[…].allow` |
| Ignore once | Drop for this activation only |
| Ignore permanent | Drop and append the name/path to `[…].ignore` |
| Abort activation | Halt, recording nothing |
| Deny permanent | Halt and append the item to `[…].deny` so future activations reject it before prompting |

The three **permanent** choices edit `user_policy.toml` in place. `min` writes
the updated file atomically (via a `.tmp` sibling) after backing up the
previous contents to `user_policy.toml.bak`, then prints `Updated <path>`. If
the file (or its directory) isn't writable, the permanent choices are hidden
and only the once/abort actions are offered; a failed save is reported as a
warning and does not, on its own, fail the activation.

### Non-interactive activation

Under [`--no-prompt`](./cli-min.md#global-flags), or when stdin/stderr is not a
TTY (CI, pipes, agents), `min` never prompts. If any item would have required a
decision, the activation aborts before contacting the daemon and prints a
ready-to-paste `user_policy.toml` snippet listing what to add. Add the rules
and re-run. When the policy already decides every item, a non-interactive
activation proceeds normally.

## Interactions and notes

- **Client-only enforcement.** The daemon never sees or runs your policy. `min`
resolves and gates contributions before (and, for daemon-surfaced items,
during) the activation round-trip; a `deny` match fails on the client.
- **Captured at activation.** The policy is read once per activation. Editing
it does not change sessions that already exist — destroy and re-activate to
pick up an edit.
- **Loadout conflicts.** When two contributors set the same variable name to
different values, composition fails; the hint is to add that name to your
policy's `ignore` list to drop all contributors of it. See
[Composition, conflicts, and policy](./loadouts.md#composition-conflicts-and-policy).
- **Diagnostics.** `min` support bundles include a **redacted**
`config/user_policy.toml.redacted` copy of the file.
- Only the `min` CLI consumes `user_policy.toml`; `mip`, `minimald`, and
`minvmd` do not.