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-lsp/Cargo.toml b/clash-lsp/Cargo.toml index 1f67cfb..3bae7a6 100644 --- a/clash-lsp/Cargo.toml +++ b/clash-lsp/Cargo.toml @@ -3,7 +3,9 @@ name = "clash-lsp" version.workspace = true edition.workspace = true license.workspace = true -publish = false +homepage.workspace = true +description = "lsp for clash policy files" + [dependencies] anyhow.workspace = true 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. +--- + +
Be an agentic engineer, not an agent babysitter.
+You define the capabilities. Clash enforces them. Your agent never sees a choice.
+Every tool call is pattern-matched against your policy — commands, arguments, file paths, network targets. No AI judgment. Same input, same result, every time.
+The most specific matching rule determines the effect. Allow runs silently. Ask prompts you. Deny blocks invisibly — Claude never knows the capability existed.
+The action executes inside an OS-level sandbox. File access, network scope, and process boundaries are enforced by the kernel, not by convention.
+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 `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/ +--- + +Everything you need to write clash policies. Policies are written in Starlark (.star), the only source format clash loads.
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