From 9e6bce8169a3c8394622e5551693d2f31070fdff Mon Sep 17 00:00:00 2001 From: Eliot Hedeman Date: Wed, 15 Apr 2026 11:17:17 -0400 Subject: [PATCH 1/3] chore: release v0.7.1 --- Cargo.lock | 22 +- Cargo.toml | 24 +- clash-npm/package.json | 8 +- clash-npm/platforms/darwin-arm64/package.json | 14 +- clash-npm/platforms/linux-arm64/package.json | 14 +- clash-npm/platforms/linux-x64/package.json | 14 +- clash-pi/package.json | 8 +- site/versions/v0.7.1/index.md | 88 +++ site/versions/v0.7.1/pages/faq.md | 98 +++ site/versions/v0.7.1/pages/quick-start.md | 98 +++ site/versions/v0.7.1/pages/reference.md | 598 ++++++++++++++++++ site/versions/v0.7.1/pages/tutorial.md | 347 ++++++++++ 12 files changed, 1291 insertions(+), 42 deletions(-) create mode 100644 site/versions/v0.7.1/index.md create mode 100644 site/versions/v0.7.1/pages/faq.md create mode 100644 site/versions/v0.7.1/pages/quick-start.md create mode 100644 site/versions/v0.7.1/pages/reference.md create mode 100644 site/versions/v0.7.1/pages/tutorial.md diff --git a/Cargo.lock b/Cargo.lock index e05352c..6423c21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,7 +728,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clash" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "base64", @@ -783,7 +783,7 @@ dependencies = [ [[package]] name = "clash-brush-builtins" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "cfg-if", @@ -808,7 +808,7 @@ dependencies = [ [[package]] name = "clash-brush-core" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "async-recursion", @@ -845,7 +845,7 @@ dependencies = [ [[package]] name = "clash-brush-interactive" -version = "0.6.2" +version = "0.7.1" dependencies = [ "bon", "clash-brush-core", @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "clash-brush-parser" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "arbitrary", @@ -886,7 +886,7 @@ dependencies = [ [[package]] name = "clash-lsp" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "clash-policy", @@ -908,7 +908,7 @@ dependencies = [ [[package]] name = "clash-policy" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "bitflags 2.11.0", @@ -927,7 +927,7 @@ dependencies = [ [[package]] name = "clash_notify" -version = "0.6.2" +version = "0.7.1" dependencies = [ "mac-notification-sys", "notify-rust", @@ -936,7 +936,7 @@ dependencies = [ [[package]] name = "clash_starlark" -version = "0.6.2" +version = "0.7.1" dependencies = [ "allocative", "anyhow", @@ -957,7 +957,7 @@ dependencies = [ [[package]] name = "claude_settings" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "figment", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "clester" -version = "0.6.2" +version = "0.7.1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5137f15..d71aae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ resolver = "3" [workspace.package] -version = "0.6.2" +version = "0.7.1" edition = "2024" rust-version = "1.94" license = "Apache-2.0" @@ -143,20 +143,20 @@ landlock = "0.4" seccompiler = "0.4" # Workspace crates (published) -clash-brush-builtins = { path = "clash-brush-builtins", version = "0.6.2" } -clash-brush-parser = { path = "clash-brush-parser", version = "0.6.2" } -clash-brush-core = { path = "clash-brush-core", version = "0.6.2" } -clash-brush-interactive = { path = "clash-brush-interactive", version = "0.6.2", features = [ +clash-brush-builtins = { path = "clash-brush-builtins", version = "0.7.1" } +clash-brush-parser = { path = "clash-brush-parser", version = "0.7.1" } +clash-brush-core = { path = "clash-brush-core", version = "0.7.1" } +clash-brush-interactive = { path = "clash-brush-interactive", version = "0.7.1", features = [ "basic", ] } # Workspace crate aliases (used by brush crates internally under original brush-* names) -brush-parser = { path = "clash-brush-parser", version = "0.6.2", package = "clash-brush-parser" } -brush-core = { path = "clash-brush-core", version = "0.6.2", package = "clash-brush-core" } +brush-parser = { path = "clash-brush-parser", version = "0.7.1", package = "clash-brush-parser" } +brush-core = { path = "clash-brush-core", version = "0.7.1", package = "clash-brush-core" } # Workspace crates (internal only) -clash-lsp = { path = "clash-lsp", version = "0.6.2" } -clash-policy = { path = "clash-policy", version = "0.6.2" } -clash_notify = { path = "clash_notify", version = "0.6.2" } -clash_starlark = { path = "clash_starlark", version = "0.6.2" } -claude_settings = { path = "claude_settings", version = "0.6.2" } +clash-lsp = { path = "clash-lsp", version = "0.7.1" } +clash-policy = { path = "clash-policy", version = "0.7.1" } +clash_notify = { path = "clash_notify", version = "0.7.1" } +clash_starlark = { path = "clash_starlark", version = "0.7.1" } +claude_settings = { path = "claude_settings", version = "0.7.1" } diff --git a/clash-npm/package.json b/clash-npm/package.json index 6da9090..9abc285 100644 --- a/clash-npm/package.json +++ b/clash-npm/package.json @@ -1,6 +1,6 @@ { "name": "@empathic/clash", - "version": "0.6.2", + "version": "0.7.1", "description": "Command Line Agent Safety Harness — policy enforcement for AI coding agents", "license": "Apache-2.0", "repository": { @@ -19,8 +19,8 @@ "README.md" ], "optionalDependencies": { - "@empathic/clash-darwin-arm64": "0.6.2", - "@empathic/clash-linux-x64": "0.6.2", - "@empathic/clash-linux-arm64": "0.6.2" + "@empathic/clash-darwin-arm64": "0.7.1", + "@empathic/clash-linux-x64": "0.7.1", + "@empathic/clash-linux-arm64": "0.7.1" } } diff --git a/clash-npm/platforms/darwin-arm64/package.json b/clash-npm/platforms/darwin-arm64/package.json index af33452..b4959aa 100644 --- a/clash-npm/platforms/darwin-arm64/package.json +++ b/clash-npm/platforms/darwin-arm64/package.json @@ -1,13 +1,19 @@ { "name": "@empathic/clash-darwin-arm64", - "version": "0.6.2", + "version": "0.7.1", "description": "Clash binary for macOS ARM64 (Apple Silicon)", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/empathic/clash" }, - "os": ["darwin"], - "cpu": ["arm64"], - "files": ["clash"] + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "files": [ + "clash" + ] } diff --git a/clash-npm/platforms/linux-arm64/package.json b/clash-npm/platforms/linux-arm64/package.json index 992ae94..37ed6e0 100644 --- a/clash-npm/platforms/linux-arm64/package.json +++ b/clash-npm/platforms/linux-arm64/package.json @@ -1,13 +1,19 @@ { "name": "@empathic/clash-linux-arm64", - "version": "0.6.2", + "version": "0.7.1", "description": "Clash binary for Linux ARM64", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/empathic/clash" }, - "os": ["linux"], - "cpu": ["arm64"], - "files": ["clash"] + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "files": [ + "clash" + ] } diff --git a/clash-npm/platforms/linux-x64/package.json b/clash-npm/platforms/linux-x64/package.json index f3c5fde..d322a57 100644 --- a/clash-npm/platforms/linux-x64/package.json +++ b/clash-npm/platforms/linux-x64/package.json @@ -1,13 +1,19 @@ { "name": "@empathic/clash-linux-x64", - "version": "0.6.2", + "version": "0.7.1", "description": "Clash binary for Linux x86_64", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/empathic/clash" }, - "os": ["linux"], - "cpu": ["x64"], - "files": ["clash"] + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "clash" + ] } diff --git a/clash-pi/package.json b/clash-pi/package.json index 0bda988..cd0c167 100644 --- a/clash-pi/package.json +++ b/clash-pi/package.json @@ -1,6 +1,6 @@ { "name": "@empathic/clash-pi", - "version": "0.6.2", + "version": "0.7.1", "description": "Clash policy enforcement extension for the Pi agent framework", "license": "Apache-2.0", "repository": { @@ -8,10 +8,12 @@ "url": "https://github.com/empathic/clash" }, "dependencies": { - "@empathic/clash": "0.6.2" + "@empathic/clash": "0.7.1" }, "pi": { - "extensions": ["extensions/clash.ts"] + "extensions": [ + "extensions/clash.ts" + ] }, "files": [ "extensions/" diff --git a/site/versions/v0.7.1/index.md b/site/versions/v0.7.1/index.md new file mode 100644 index 0000000..dc18bbc --- /dev/null +++ b/site/versions/v0.7.1/index.md @@ -0,0 +1,88 @@ +--- +layout: base.njk +title: Clash +description: Write rules for your AI agent. Clash enforces them. +--- + +
+

clash

+

Be an agentic engineer, not an agent babysitter.

+

You define the capabilities. Clash enforces them. Your agent never sees a choice.

+
+ +
Clash for Claude
+ +
+ +Claude interrupts us constantly asking whether it's okay to run some command. +This is why `--dangerously-skip-permissions` is so powerful: we can achieve so +much more per unit prompt. + +Every tool call your agent makes requires a decision. Right now, that decision +is yours - *every single time* or **not at all**. + +Think of Clash as a new `--safely-skip-permissions` flag. A way to choose _how_ +to run anything safely, not just whether Claude can run it **as *you***. + +Clash intercepts every tool call Claude makes and runs it through your policy — before anything executes. + +
+
+

Match

+

Every tool call is pattern-matched against your policy — commands, arguments, file paths, network targets. No AI judgment. Same input, same result, every time.

+
+
+

Decide

+

The most specific matching rule determines the effect. Allow runs silently. Ask prompts you. Deny blocks invisibly — Claude never knows the capability existed.

+
+
+

Sandbox

+

The action executes inside an OS-level sandbox. File access, network scope, and process boundaries are enforced by the kernel, not by convention.

+
+
+ +
+ +
Get Started
+ +
+ +## Get started with Clash for Claude + +Install: +```bash +curl -fsSL https://raw.githubusercontent.com/empathic/clash/main/install.sh | bash +``` + +Initialize: +``` +clash init +``` + +Run w/ Claude: +``` +claude +``` + +Three commands. One binary, one policy file, full enforcement. See the [Quick Start](/quick-start/) for details. + +
+ +
Agent Support
+ +
+ +## Agent support + +| Agent | Status | +|---|---| +| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | **Supported** | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | **Protocol ready** | +| [Codex CLI](https://github.com/openai/codex) | **Protocol ready** | +| [Amazon Q CLI](https://github.com/aws/amazon-q-developer-cli) | **Protocol ready** | +| [OpenCode](https://github.com/opencode-ai/opencode) | **Protocol ready** | +| [Copilot CLI](https://github.com/github/copilot-cli) | **Protocol ready** | + +Use `clash init --agent ` to set up any supported agent. [Contributions welcome](https://github.com/empathic/clash). + +
diff --git a/site/versions/v0.7.1/pages/faq.md b/site/versions/v0.7.1/pages/faq.md new file mode 100644 index 0000000..783e6cc --- /dev/null +++ b/site/versions/v0.7.1/pages/faq.md @@ -0,0 +1,98 @@ +--- +layout: base.njk +title: FAQ +description: Frequently asked questions about Clash. +permalink: /faq/ +--- + +

FAQ

+

Quick answers to common questions.

+ +## Is Clash only for AI agents? + +No. Clash is a general-purpose sandboxing and policy engine. You can use `clash sandbox exec` to sandbox any command — build scripts, install scripts, untrusted binaries, anything. The AI agent integration is one application of the same underlying engine. + +--- + +## How is this different from Docker or Firejail? + +Clash is lighter and more targeted. It doesn't virtualize anything — it applies Landlock (Linux) or Seatbelt (macOS) restrictions directly to a process. No container images, no namespaces, no root required. You write a few rules about which paths and network access a command should have, and the kernel enforces them. It's closer to a fine-grained `sandbox-exec` than a container runtime. + +--- + +## How is this different from Claude Code's built-in permissions? + +Claude Code gives you per-tool toggles: allow a tool entirely, or get prompted every time. Clash gives you **pattern-matching rules within tools**: allow `git status` while denying `git push`, allow reads under your project while blocking writes to `.env`, allow network access to `github.com` while blocking everything else. Plus kernel-enforced sandboxes, policy composition, and per-project layering. + +--- + +## Which agents does Clash support? + +**Claude Code** is fully supported today. Gemini CLI, Codex CLI, Amazon Q CLI, OpenCode, and Copilot CLI have protocol-ready integrations. But Clash also works standalone — `clash sandbox exec` sandboxes any command, no agent required. The policy engine is agent-independent; only the hook layer is specific to each agent. See the [agent support table]({{ version.pathPrefix }}/#agent-support). + +--- + +## Will it slow things down? + +No. Policies compile into a decision tree at load time. Evaluation is in-process with no network round-trip. Sandbox setup adds a few milliseconds at process launch — then the kernel handles enforcement with zero runtime overhead. + +--- + +## What happens if my policy has a bug? + +Clash blocks **everything** when the policy fails to compile. A broken policy should never silently approve things. Run `clash policy validate` to find the error, or `clash init` to start over. + +--- + +## Can I share policies with my team? + +Yes. Commit a **project-level** policy at `/.clash/policy.star`. Team members pick it up automatically. Individuals can still override with user-level or session-level policies. + +--- + +## How does the kernel sandbox work? + +When a rule includes a sandbox, Clash generates OS-level restrictions: + +- **Linux:** Landlock constrains filesystem access; seccomp + proxy filters network calls. +- **macOS:** Seatbelt profiles restrict filesystem and network access. + +Restrictions are inherited by all child processes and cannot be bypassed. Run `clash sandbox check` to verify your platform supports it. + +--- + +## Do rules apply to subprocesses? + +Exec rules match only the **top-level command**. If `make deploy` internally runs `git push`, the deny rule for `git push` won't fire. However, **sandbox restrictions** on filesystem and network access are enforced on all child processes at the kernel level — that's the whole point. + +--- + +## How do I turn it off temporarily? + +```bash +CLASH_DISABLE=1 claude +``` + +Clash stays installed but becomes a pass-through — no enforcement, no prompts. Unset the variable to re-enable. + +--- + +## Which platforms work? + +- **macOS** — Apple Silicon and Intel +- **Linux** — x86_64 and aarch64 +- **Windows** — not supported + +Kernel sandboxing needs Landlock (Linux 5.13+) or Seatbelt (macOS). Clash works without sandboxing on older kernels, but sandbox rules will be ignored. + +--- + +## How do I uninstall? + +```bash +clash uninstall +``` + +This removes the Claude Code plugin, the status line, policy files (`~/.clash/`), and the binary — regardless of how it was installed. Use `clash uninstall -y` to skip confirmation prompts. + +After uninstalling, Claude Code reverts to its built-in permission model. diff --git a/site/versions/v0.7.1/pages/quick-start.md b/site/versions/v0.7.1/pages/quick-start.md new file mode 100644 index 0000000..794bf9e --- /dev/null +++ b/site/versions/v0.7.1/pages/quick-start.md @@ -0,0 +1,98 @@ +--- +layout: base.njk +title: Quick Start +description: Get up and running with Clash in under two minutes +permalink: /quick-start/ +--- + +

Quick Start

+

From zero to enforced policy in three steps.

+ +## 1. Install + +```bash +curl -fsSL https://raw.githubusercontent.com/empathic/clash/main/install.sh | bash +``` + +On Intel Mac, use `cargo install clash` instead. + +## 2. Initialize + +```bash +clash init +``` + +This creates a policy file at `~/.clash/policy.star`, installs the Claude Code plugin, and configures permissions so Clash is the sole decision-maker. + +To skip the interactive editor and use sensible defaults: + +```bash +clash init --quick +``` + +## 3. Use it + +```bash +claude +``` + +Every tool call now passes through your policy. Check that it's working: + +```bash +clash status +``` + +To see which rule matches a specific command: + +```bash +clash explain bash "git push" +``` + +--- + +## What you get out of the box + +The default policy: + +- allow file reads and writes within your project directory +- allow all command execution inside a sandbox +- deny `git push --force`, `git push --force-with-lease`, `git reset --hard` +- ask for network access (`WebFetch`, `WebSearch`) +- ask for everything else not covered by a rule + +--- + +## Customize it + +Open your policy in your editor: + +```bash +clash policy edit --raw +``` + +Or use the CLI to add rules directly: + +```bash +# allow cargo commands +clash policy allow "cargo build" +clash policy allow "cargo test" + +# block dangerous operations +clash policy deny "rm -rf" + +# check what you've got +clash policy list +``` + +Format your policy file: + +```bash +clash fmt +``` + +--- + +## Next steps + +- [Tutorial](/tutorial/) — Build a policy from scratch, step by step +- [Reference](/reference/) — Full documentation for all policy builders and CLI commands diff --git a/site/versions/v0.7.1/pages/reference.md b/site/versions/v0.7.1/pages/reference.md new file mode 100644 index 0000000..48bc2c0 --- /dev/null +++ b/site/versions/v0.7.1/pages/reference.md @@ -0,0 +1,598 @@ +--- +layout: base.njk +title: Reference +description: Complete reference for Clash policy language, sandboxes, and compiled schema +permalink: /reference/ +--- + +

Reference

+

Everything you need to write clash policies. Policies are written in Starlark (.star), the only source format clash loads.

+ +## Effects + +Every rule ends with an effect: + +- allow — auto-approve the action +- deny — block the action +- ask — prompt the user for confirmation + +```python +policy("default", { + "Bash": { + "git": allow(), + "git": {"push": deny()}, + "git": {"commit": ask()}, + }, +}) +``` + +**First match wins.** Rules are evaluated in order — the first matching rule determines the effect. Put specific rules (like denies) before broad ones (like allows). + +--- + +## Domains + +Clash matches rules across three domains. A single rule can cover multiple tools. + +### Exec — shell commands + +```python +policy("default", { + "Bash": { + "git": allow(), + "git": {"push": deny()}, + "cargo": {"test": allow()}, + ("cargo", "rustc"): allow(), # multiple binaries + }, +}) +``` + +Policy dicts build rules by nesting tool names, binary names, and subcommands. Deeper nesting = more specific matching. + +**Scope:** Exec rules evaluate the top-level command the agent invokes. They do not apply to child processes spawned by that command. Sandbox restrictions on filesystem and network access *are* enforced on all child processes at the kernel level. + +#### Command trees + +For commands with many subcommands, nest dicts and use tuple keys: + +```python +policy("default", { + "Bash": { + "git": { + "push": deny(), + ("pull", "fetch"): allow(), + "remote": { + "add": ask(), + }, + }, + }, +}) +``` + +#### Typed match keys: `Mode()` and `Tool()` + +Use `Mode()` to apply different rules based on the agent's current permission mode (e.g. plan mode vs code mode). Use `Tool()` as an explicit alternative to raw strings for tool names: + +```python +load("@clash//std.star", "allow", "deny", "Mode", "Tool", "policy") + +policy("default", { + Mode("plan"): { + Tool("Read"): allow(), + Tool("ExitPlanMode"): allow(), + }, + Tool("Bash"): {"git": allow()}, + "WebSearch": deny(), +}) +``` + +### Fs — file operations + +File access for agent tools is controlled via sandboxes. Attach a sandbox to tool rules: + +```python +load("@clash//std.star", "allow", "deny", "policy", "sandbox", "subpath") + +project_sandbox = sandbox( + name = "project", + default = deny(), + fs = { + subpath("$PWD", follow_worktrees = True): allow("rwc"), # project dir, worktree-aware + "$HOME/.ssh": allow("r"), # read-only ~/.ssh + }, +) + +policy("default", { + ("Read", "Glob", "Grep"): allow(sandbox = project_sandbox), + ("Write", "Edit"): allow(sandbox = project_sandbox), +}) +``` + +The `fs` dict in a sandbox maps path strings (or `subpath()` for worktree support) to capabilities. The fs domain maps to agent tools: `Read` / `Glob` / `Grep` → `fs read`, `Write` / `Edit` → `fs write`. + +### Net — network access + +```python +domains({"github.com": allow()}) +domains({"github.com": allow(), "crates.io": allow()}) +``` + +The net domain maps to: `WebFetch` → `net` with the URL's domain, `WebSearch` → `net` with wildcard domain. + +### Tool — agent tools + +```python +policy("default", { + "WebSearch": deny(), + ("Read", "Glob", "Grep"): allow(), + ("Skill", "Agent"): allow(), +}) +``` + +Use tool names directly as dict keys to control agent tools by name. This works for any tool, including those that don't map to exec/fs/net capabilities (e.g., `Skill`, `Agent`). + +--- + +## Patterns + +In the compiled match tree, patterns are used inside condition nodes to match against observable values. + +### Wildcard + +`"wildcard"` matches anything: + +```json +{ "condition": { "observe": "tool_name", "pattern": "wildcard", "children": [{ "decision": { "allow": null } }] } } +``` + +### Literal + +`{ "literal": }` matches a resolved value exactly: + +```json +{ "condition": { "observe": { "positional_arg": 0 }, "pattern": { "literal": { "literal": "git" } }, "children": [...] } } +{ "condition": { "observe": "tool_name", "pattern": { "literal": { "literal": "Bash" } }, "children": [...] } } +``` + +### Regex + +`{ "regex": "pattern" }` for flexible matching: + +```json +{ "condition": { "observe": { "positional_arg": 0 }, "pattern": { "regex": "^cargo-.*" }, "children": [...] } } +``` + +### Combinators + +`{ "any_of": [...] }` matches any sub-pattern. `{ "not": }` negates: + +```json +{ "condition": { "observe": "tool_name", "pattern": { "any_of": [ + { "literal": { "literal": "Read" } }, + { "literal": { "literal": "Glob" } }, + { "literal": { "literal": "Grep" } } +] }, "children": [{ "decision": { "allow": null } }] } } +``` + +--- + +## Values + +Values appear inside `Literal` patterns and are resolved at eval time: + +| Form | JSON | Description | +|---|---|---| +| Literal string | `{ "literal": "git" }` | A constant string value | +| Environment var | `{ "env": "HOME" }` | Resolved from environment at eval time | +| Path join | `{ "path": [{ "env": "HOME" }, { "literal": ".ssh" }] }` | Segments joined with `/` | + +--- + +## Precedence + +Rules use **first-match semantics**: the first matching rule wins. Order matters — put specific rules before broad ones. + +Example: + +```python +policy("default", { + "Bash": { + "git": { + "push": deny(), + glob("**"): allow(), + }, + }, +}) +``` + +`git push origin main` matches the deny first (listed first, matches). `git status` skips the deny (doesn't match "push") and matches the wildcard allow. + +If the rules were reversed, `git push` would match the allow first and the deny would never fire. + +When a request matches rules in multiple domains, deny-overrides applies across domains: deny > ask > allow. + +--- + +## Policy composition + +In Starlark, break policies into reusable pieces using variables and `merge()`: + +```python +# ~/.clash/safe_git.star +load("@clash//std.star", "allow", "ask", "deny", "glob") + +safe_git_rules = { + "Bash": { + "git": { + "push": deny(), + "reset": deny(), + "commit": ask(), + glob("**"): allow(), + }, + }, +} +``` + +```python +# ~/.clash/policy.star +load("@clash//std.star", "allow", "deny", "domains", "merge", "policy", "sandbox", "subpath") +load("safe_git.star", "safe_git_rules") + +project_sandbox = sandbox( + name = "project", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, +) + +settings(default = deny()) + +policy("default", merge( + {("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = project_sandbox)}, + safe_git_rules, + domains({"github.com": allow(), "crates.io": allow()}), +)) +``` + +Starlark `load()` imports values from other `.star` files. All composition (function calls, `merge()`, imports) resolves at compile time. + +### One format: `.star` for everything + +Clash policies are written in Starlark. The same `.star` file is edited by humans and mutated by CLI commands like `clash policy allow`, `clash policy deny`, and `clash policy remove`, which round-trip the file using a Starlark AST formatter so hand-written structure is preserved. + +#### Migrating from `policy.json` + +Earlier versions of clash supported a JSON-based `policy.json` format. To migrate: + +```sh +clash policy convert # writes policy.star alongside policy.json +clash policy convert --replace # also removes the old policy.json +``` + +After conversion, only `policy.star` is loaded. + +### Updating policies + +The `update()` method combines two policies. In `a.update(b)`, `b`'s default effect is used, tree nodes from both are concatenated (`a`'s first, then `b`'s), and sandboxes are merged (first defined wins on name conflicts). + +```python +load("@clash//builtin.star", "base") +load("@clash//std.star", "allow", "deny", "domains", "merge", "policy", "sandbox", "subpath") + +project_sandbox = sandbox( + name = "project", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, +) + +settings(default = deny()) + +policy("default", merge( + {("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = project_sandbox)}, + {"Bash": {"git": allow()}}, + domains({"github.com": allow()}), +)) +``` + +### Built-in policy (`@clash//builtin.star`) + +The `builtins` export from `@clash//builtin.star` bundles rules for: + +- **Clash CLI** — allows `clash status`, `clash policy list/show/explain`, and `clash bug` with appropriate sandboxes +- **Claude Code tools** — allows interactive tools (`Agent`, `Skill`, `AskUserQuestion`, `ToolSearch`, etc.) with a sandbox scoped to `~/.claude` + +Merge with `builtins` to get sensible defaults. If you don't, you'll need your own rules for these tools. + +--- + +## Sandbox policies + +Allowed exec rules can carry a sandbox that constrains what the spawned process can access at the kernel level (Landlock on Linux, Seatbelt on macOS). + +### Defining a sandbox + +In Starlark, use the `sandbox()` builder and pass it to `allow()` / `deny()` / `ask()` via the `sandbox` keyword: + +```python +load("@clash//std.star", "allow", "deny", "policy", "sandbox") + +cargo_env = sandbox( + name = "cargo", + default = deny(), + fs = {"$PWD": allow("rwc")}, + net = allow(), +) + +settings(default = deny()) + +policy("default", { + "Bash": {"cargo": allow(sandbox = cargo_env)}, +}) +``` + +### What sandboxes enforce + +Sandbox restrictions on **filesystem and network access** are inherited by all child processes and cannot be bypassed. However, sandboxes do not enforce exec-level argument matching on child processes. + +### Sandbox network modes + +- `net = allow()` in a sandbox — allows all network access +- `net = [domains({"localhost": allow()})]` — localhost-only, enforced at the kernel level +- `net = [domains({"domain.com": allow()})]` — domain-filtered via local HTTP proxy +- `net = deny()` or omitted — denies all network access + +--- + +## Policy settings + +The `settings()` function configures global policy behavior. It is optional; defaults apply when omitted. + +```python +settings(default=deny(), on_sandbox_violation="stop") +``` + +### `default` + +The default effect when no rule matches. Accepts `allow()`, `deny()`, or `ask()`. Defaults to `"deny"`. + +### `default_sandbox` + +A sandbox to apply by default to all shell command rules that do not specify their own sandbox. + +### `on_sandbox_violation` + +Controls model behavior when a sandbox blocks an operation. Added as a parameter to `settings()`: + +```python +settings(default=deny(), on_sandbox_violation="stop") +``` + +Values: +- `"stop"` (default) — Tell the model to stop and suggest a policy fix. Don't retry. +- `"workaround"` — Tell the model to try an alternative approach. If no workaround is possible, suggest the policy fix. +- `"smart"` — Let the model assess context to decide whether to suggest a fix or find an alternative. + +### `harness_defaults` + +Controls whether clash automatically injects rules that allow the agent to access its own infrastructure directories. Defaults to `True`. + +When enabled, clash injects rules at the lowest priority (after all user-defined rules) to allow access to: + +| Path | Permissions | Purpose | +|------|-------------|---------| +| `~/.claude/` | read, write, create, delete | Memories, settings, plugin cache, skills | +| `/.claude/` | read only | Project config | +| `/` | read, write, create, delete | Session transcripts | + +Your rules always take precedence over harness defaults. + +```python +settings(harness_defaults=False) # disable harness defaults +``` + +Or via environment variable: `CLASH_NO_HARNESS_DEFAULTS=1`. + +`clash status` hides harness rules by default and shows a count. Use `clash status --verbose` to see them tagged with `[harness]`. + +--- + +## Common recipes + +### Conservative (untrusted projects) + +```python +load("@clash//std.star", "allow", "deny", "policy", "sandbox", "subpath") + +readonly_sandbox = sandbox( + name = "readonly", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("r")}, +) + +settings(default = deny()) + +policy("default", { + ("Read", "Glob", "Grep"): allow(sandbox = readonly_sandbox), +}) +``` + +### Developer-friendly + +```python +load("@clash//std.star", "allow", "ask", "deny", "domains", "merge", "policy", "sandbox", "subpath") + +project_sandbox = sandbox( + name = "project", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, +) + +settings(default = ask()) + +policy("default", merge( + {("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = project_sandbox)}, + { + "Bash": { + ("cargo", "npm"): allow(), + "git": { + ("status", "diff", "log", "add"): allow(), + "commit": ask(), + ("push", "reset"): deny(), + }, + "sudo": deny(), + "rm": {"-rf": deny()}, + }, + }, + domains({"github.com": allow(), "crates.io": allow(), "npmjs.com": allow()}), +)) +``` + +### Full trust with guardrails + +```python +load("@clash//std.star", "allow", "ask", "deny", "policy") + +settings(default = allow()) + +policy("default", { + "Bash": { + "git": { + "push": {"--force": deny()}, + "reset": {"--hard": deny()}, + }, + "rm": {"-rf": deny()}, + "sudo": deny(), + }, +}) +``` + +### Sandboxed build tools + +```python +load("@clash//std.star", "allow", "deny", "domains", "policy", "sandbox", "subpath") + +cargo_env = sandbox( + name = "cargo", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, + net = allow(), +) +npm_env = sandbox( + name = "npm", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, + net = [domains({"registry.npmjs.org": allow()})], +) +readonly_sandbox = sandbox( + name = "readonly", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("r")}, +) + +settings(default = deny()) + +policy("default", { + ("Read", "Glob", "Grep"): allow(sandbox = readonly_sandbox), + "Bash": { + "cargo": allow(sandbox = cargo_env), + "npm": allow(sandbox = npm_env), + }, +}) +``` + +--- + +## Policy schema (JSON IR) + +JSON IR schema for compiled clash policies. Policies are authored as Starlark (`.star`) files and compiled to this format. + +### Document structure + +```json +{ + "schema_version": 5, + "default_effect": "", + "sandboxes": { "": }, + "tree": [ , ... ] +} +``` + +| Field | Type | Description | +|---|---|---| +| `schema_version` | integer | Internal version identifier | +| `default_effect` | string | Effect when no rule matches: `"allow"`, `"deny"`, or `"ask"` | +| `sandboxes` | object | Named sandbox definitions (may be empty) | +| `tree` | array | Root-level nodes of the match tree | + +### Nodes + +The tree is a uniform trie of two node types: + +#### Condition + +Observe a value from the query context, test against a pattern, recurse into children on match: + +```json +{ "condition": { "observe": , "pattern": , "children": [ , ... ] } } +``` + +| Field | Type | Description | +|---|---|---| +| `observe` | observable | What to extract from the query context | +| `pattern` | pattern | What to test the observed value against | +| `children` | array of nodes | Evaluated (in order) if the pattern matches | + +#### Decision + +A leaf node that produces an effect: + +```json +{ "decision": { "allow": null } } +{ "decision": "deny" } +{ "decision": { "ask": null } } +{ "decision": { "allow": "" } } +``` + +| Form | Description | +|---|---| +| `{ "allow": null }` | Allow without sandbox | +| `{ "allow": "" }` | Allow with named sandbox | +| `"deny"` | Deny | +| `{ "ask": null }` | Ask the user | +| `{ "ask": "" }` | Ask the user, with sandbox if approved | + +### Observables + +What to extract from the query context for pattern matching. + +```json +"tool_name" +"hook_type" +"agent_name" +"mode" +{ "positional_arg": 0 } +"has_arg" +{ "named_arg": "file_path" } +{ "nested_field": ["input", "url"] } +``` + +| Observable | JSON | Description | +|---|---|---| +| Tool name | `"tool_name"` | The agent tool being invoked (e.g. "Bash", "Read") | +| Hook type | `"hook_type"` | The hook event type | +| Agent name | `"agent_name"` | The agent identifier | +| Mode | `"mode"` | The agent's current permission mode (e.g. "plan", "code") | +| Positional arg | `{ "positional_arg": N }` | Nth positional argument (0-indexed) | +| Has arg | `"has_arg"` | True if any positional arg matches the pattern | +| Named arg | `{ "named_arg": "key" }` | Value of a named argument | +| Nested field | `{ "nested_field": ["a", "b"] }` | Path into structured tool_input JSON | + +### Evaluation + +Evaluation is a single DFS pass over the tree: + +1. For each node in children (in order): + - **Decision**: return the decision immediately + - **Condition**: extract the observable value from the query context, test against the pattern. If it matches, recurse into children. If a child produces a decision, return it. Otherwise, backtrack and try the next sibling. +2. If no node produces a decision, return the `default_effect`. + +First-match semantics: the first matching path through the tree wins. Specificity is encoded by sibling order — put more specific conditions before broader ones. diff --git a/site/versions/v0.7.1/pages/tutorial.md b/site/versions/v0.7.1/pages/tutorial.md new file mode 100644 index 0000000..48c6201 --- /dev/null +++ b/site/versions/v0.7.1/pages/tutorial.md @@ -0,0 +1,347 @@ +--- +layout: base.njk +title: Tutorial +description: Build a Clash policy from scratch, step by step +permalink: /tutorial/ +--- + +

Tutorial

+

Build a real policy from an empty file. Each step adds one concept.

+ +## Prerequisites + +Clash installed and initialized. If not, run through the [Quick Start](/quick-start/) first. + +You should have a policy file at `~/.clash/policy.star`. Open it: + +```bash +clash policy edit --raw +``` + +Replace whatever's there with the code below as we go. Every time you save, Clash picks up the changes immediately — no restart needed. + +--- + +## Step 1: Start with deny-all + +The safest starting point. Block everything, then open up what you need. + +```python +settings(default = deny()) +policy("default", {}) +``` + +Every policy file calls `settings()` to set the default effect and `policy()` to register rules. The `default = deny()` in `settings()` means deny everything when no rule matches — and our empty dict `{}` means no rules yet. + +Save this file and try running your agent. Every tool call will be blocked. That's the point — we'll open it up from here. + +--- + +## Step 2: Allow safe read operations + +Your agent needs to read files to be useful. Let's allow that. + +```python +settings(default = deny()) + +policy("default", { + ("Read", "Glob", "Grep"): allow(), +}) +``` + +The dict keys are tool names. `Read`, `Glob`, and `Grep` are the read-only file tools — allowing all three lets the agent browse and read files without being able to write or edit them. + +Test it: + +```bash +clash explain glob "src/**/*.rs" +``` + +You should see an allow decision. + +--- + +## Step 3: Allow writes to your project + +Read-only is safe but not very productive. Let's allow writes too. + +```python +load("@clash//std.star", "allow", "deny", "policy", "sandbox", "subpath") + +project_sandbox = sandbox( + name = "project", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, +) + +settings(default = deny()) + +policy("default", { + ("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = project_sandbox), +}) +``` + +The sandbox restricts file tool access to your project directory. Files outside your project are still denied. + +--- + +## Step 4: Allow commands + +Your agent needs to run build tools and git. Nest dicts to define rules as a tree of tool names and subcommands: + +```python +load("@clash//std.star", "allow", "deny", "policy", "sandbox", "subpath") + +project_sandbox = sandbox( + name = "project", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, +) + +settings(default = deny()) + +policy("default", { + ("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = project_sandbox), + + "Bash": { + "git": { + ("add", "commit", "diff", "log", "status", "branch"): allow(), + "push": deny(), + "reset": {"--hard": deny()}, + }, + "cargo": { + ("build", "test", "check", "clippy", "fmt"): allow(), + "publish": deny(), + }, + }, +}) +``` + +Dict keys are matched against positional arguments — deeper nesting = more specific matches: + +- Tuples like `("add", "commit", "diff")` match any of those subcommands +- Nested dicts like `"reset": {"--hard": deny()}` match deeper argument patterns +- Unmatched subcommands fall through to the policy default (deny, in our case) + +Verify your rules: + +```bash +clash explain bash "git status" # → allow +clash explain bash "git push" # → deny +clash explain bash "git stash" # → deny (no rule, falls to default) +``` + +--- + +## Step 5: Add a default for everything else + +Denying everything unmatched is safe but noisy when you're actively working. Switch the default to `ask` so your agent can request approval for things you haven't written rules for yet: + +```python +load("@clash//std.star", "allow", "ask", "deny", "policy", "sandbox", "subpath") + +project_sandbox = sandbox( + name = "project", + default = deny(), + fs = {subpath("$PWD", follow_worktrees = True): allow("rwc")}, +) + +settings(default = ask()) + +policy("default", { + ("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = project_sandbox), + + "Bash": { + "git": { + ("add", "commit", "diff", "log", "status", "branch"): allow(), + "push": deny(), + "reset": {"--hard": deny()}, + }, + "cargo": { + ("build", "test", "check", "clippy", "fmt"): allow(), + "publish": deny(), + }, + }, +}) +``` + +Now unmatched commands prompt you instead of silently failing. As you work, you'll notice which commands you're approving repeatedly — add rules for those. + +--- + +## Step 6: Add sandboxes + +Rules control whether a command runs. Sandboxes control what it can access *while* it runs — filesystem paths and network access, enforced at the OS level. + +```python +load("@clash//std.star", "allow", "ask", "deny", "policy", "sandbox", "subpath") + +dev_sandbox = sandbox( + name = "dev", + default = deny(), + fs = { + subpath("$PWD", follow_worktrees = True): allow("rwc"), + "$HOME/.cargo": allow("rwc"), + "$HOME/.rustup": allow("r"), + "$TMPDIR": allow(), + }, + net = allow(), +) + +settings(default = ask()) + +policy("default", { + ("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = dev_sandbox), + + "Bash": { + "git": { + ("add", "commit", "diff", "log", "status", "branch"): allow(), + "push": deny(), + "reset": {"--hard": deny()}, + }, + "cargo": { + ("build", "test", "check", "clippy", "fmt"): allow(sandbox = dev_sandbox), + "publish": deny(), + }, + }, +}) +``` + +The `sandbox()` builder defines a restricted environment: + +- `fs` is a dict mapping paths to capabilities. Keys are path strings (or `subpath()` for worktree support); values are the allowed capabilities. +- `net` controls network access — `allow()`, `deny()`, or a list of `domains()` +- `default = deny()` blocks access to anything not listed + +Attach a sandbox to an effect with `allow(sandbox = dev_sandbox)`. When cargo runs, it can access your project, cargo's cache, rustup, and temp — nothing else. This is enforced at the kernel level. Child processes inherit the same restrictions. + +--- + +## Step 7: Restrict network access + +Instead of allowing all network access in your sandbox, restrict it to specific domains: + +```python +load("@clash//std.star", "allow", "ask", "deny", "domains", "policy", "sandbox", "subpath") + +dev_sandbox = sandbox( + name = "dev", + default = deny(), + fs = { + subpath("$PWD", follow_worktrees = True): allow("rwc"), + "$HOME/.cargo": allow("rwc"), + "$HOME/.rustup": allow("r"), + "$TMPDIR": allow(), + }, + net = [ + domains({ + "github.com": allow(), + "crates.io": allow(), + "*.crates.io": allow(), + }), + ], +) + +settings(default = ask()) + +policy("default", { + ("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = dev_sandbox), + + "Bash": { + "git": { + ("add", "commit", "diff", "log", "status", "branch"): allow(), + "push": deny(), + "reset": {"--hard": deny()}, + }, + "cargo": { + ("build", "test", "check", "clippy", "fmt"): allow(sandbox = dev_sandbox), + "publish": deny(), + }, + }, +}) +``` + +Now cargo can reach GitHub and crates.io but nothing else. The `*` prefix matches subdomains. + +--- + +## Step 8: Use the builtins + +Clash ships with built-in rules for its own CLI and common Claude Code tools. Instead of writing rules for `clash status` or the `Agent` tool yourself, merge with the builtins: + +```python +load("@clash//builtin.star", "builtins") +load("@clash//std.star", "allow", "ask", "deny", "domains", "merge", "policy", "sandbox", "subpath") + +dev_sandbox = sandbox( + name = "dev", + default = deny(), + fs = { + subpath("$PWD", follow_worktrees = True): allow("rwc"), + "$HOME/.cargo": allow("rwc"), + "$HOME/.rustup": allow("r"), + "$TMPDIR": allow(), + }, + net = [ + domains({ + "github.com": allow(), + "crates.io": allow(), + "*.crates.io": allow(), + }), + ], +) + +settings(default = ask()) + +policy("default", merge(builtins, { + ("Read", "Write", "Edit", "Glob", "Grep"): allow(sandbox = dev_sandbox), + + "Bash": { + "git": { + ("add", "commit", "diff", "log", "status", "branch"): allow(), + "push": deny(), + "reset": {"--hard": deny()}, + }, + "cargo": { + ("build", "test", "check", "clippy", "fmt"): allow(sandbox = dev_sandbox), + "publish": deny(), + }, + }, +})) +``` + +`builtins` is a dict exported from `@clash//builtin.star`. Merging it puts the built-in rules into your policy alongside yours. + +--- + +## Verify your policy + +Check the full policy status: + +```bash +clash status +``` + +Test specific commands against your rules: + +```bash +clash explain bash "cargo test" # → allow (sandbox: dev) +clash explain bash "git push --force" # → deny +clash explain read "~/.ssh/id_rsa" # → ask (no rule, falls to default) +``` + +Format your policy file: + +```bash +clash fmt +``` + +--- + +## What to do next + +**Start a session and pay attention to prompts.** Every time Clash asks you to approve something, that's a rule you might want to add. The goal is to eliminate prompts for commands you trust while keeping blocks on commands you don't. + +- Use `clash policy allow "command"` to add rules from the CLI without editing the file +- See the [Reference](/reference/) for the full list of policy builders +- Browse the [example policies](https://github.com/empathic/clash/tree/main/examples) for Python, Node, and Rust development setups From c4589fa457bac9fd7c5f6e770be27c5e0a25504a Mon Sep 17 00:00:00 2001 From: Eliot Hedeman Date: Wed, 15 Apr 2026 11:19:45 -0400 Subject: [PATCH 2/3] publish clash-lsp --- clash-lsp/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/clash-lsp/Cargo.toml b/clash-lsp/Cargo.toml index 1f67cfb..c2dd6ce 100644 --- a/clash-lsp/Cargo.toml +++ b/clash-lsp/Cargo.toml @@ -3,7 +3,6 @@ name = "clash-lsp" version.workspace = true edition.workspace = true license.workspace = true -publish = false [dependencies] anyhow.workspace = true From bd6b38bdf977d630aae9c4437f7e07f784678c8f Mon Sep 17 00:00:00 2001 From: Eliot Hedeman Date: Wed, 15 Apr 2026 11:22:47 -0400 Subject: [PATCH 3/3] fix cargo toml --- clash-lsp/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clash-lsp/Cargo.toml b/clash-lsp/Cargo.toml index c2dd6ce..3bae7a6 100644 --- a/clash-lsp/Cargo.toml +++ b/clash-lsp/Cargo.toml @@ -3,6 +3,9 @@ name = "clash-lsp" version.workspace = true edition.workspace = true license.workspace = true +homepage.workspace = true +description = "lsp for clash policy files" + [dependencies] anyhow.workspace = true