diff --git a/Cargo.lock b/Cargo.lock
index e05352c..07fa678 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -728,7 +728,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clash"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"base64",
@@ -783,7 +783,7 @@ dependencies = [
[[package]]
name = "clash-brush-builtins"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"cfg-if",
@@ -808,7 +808,7 @@ dependencies = [
[[package]]
name = "clash-brush-core"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -845,7 +845,7 @@ dependencies = [
[[package]]
name = "clash-brush-interactive"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"bon",
"clash-brush-core",
@@ -863,7 +863,7 @@ dependencies = [
[[package]]
name = "clash-brush-parser"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"arbitrary",
@@ -886,7 +886,7 @@ dependencies = [
[[package]]
name = "clash-lsp"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"clash-policy",
@@ -908,7 +908,7 @@ dependencies = [
[[package]]
name = "clash-policy"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"bitflags 2.11.0",
@@ -927,7 +927,7 @@ dependencies = [
[[package]]
name = "clash_notify"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"mac-notification-sys",
"notify-rust",
@@ -936,7 +936,7 @@ dependencies = [
[[package]]
name = "clash_starlark"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"allocative",
"anyhow",
@@ -957,7 +957,7 @@ dependencies = [
[[package]]
name = "claude_settings"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"figment",
@@ -971,7 +971,7 @@ dependencies = [
[[package]]
name = "clester"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"anyhow",
"clap",
diff --git a/Cargo.toml b/Cargo.toml
index 5137f15..e8a6b92 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,7 +15,7 @@ members = [
resolver = "3"
[workspace.package]
-version = "0.6.2"
+version = "0.7.0"
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.0" }
+clash-brush-parser = { path = "clash-brush-parser", version = "0.7.0" }
+clash-brush-core = { path = "clash-brush-core", version = "0.7.0" }
+clash-brush-interactive = { path = "clash-brush-interactive", version = "0.7.0", 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.0", package = "clash-brush-parser" }
+brush-core = { path = "clash-brush-core", version = "0.7.0", 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.0" }
+clash-policy = { path = "clash-policy", version = "0.7.0" }
+clash_notify = { path = "clash_notify", version = "0.7.0" }
+clash_starlark = { path = "clash_starlark", version = "0.7.0" }
+claude_settings = { path = "claude_settings", version = "0.7.0" }
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
diff --git a/site/versions/v0.7.0/index.md b/site/versions/v0.7.0/index.md
new file mode 100644
index 0000000..dc18bbc
--- /dev/null
+++ b/site/versions/v0.7.0/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.0/pages/faq.md b/site/versions/v0.7.0/pages/faq.md
new file mode 100644
index 0000000..783e6cc
--- /dev/null
+++ b/site/versions/v0.7.0/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.0/pages/quick-start.md b/site/versions/v0.7.0/pages/quick-start.md
new file mode 100644
index 0000000..794bf9e
--- /dev/null
+++ b/site/versions/v0.7.0/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.0/pages/reference.md b/site/versions/v0.7.0/pages/reference.md
new file mode 100644
index 0000000..48bc2c0
--- /dev/null
+++ b/site/versions/v0.7.0/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.0/pages/tutorial.md b/site/versions/v0.7.0/pages/tutorial.md
new file mode 100644
index 0000000..48c6201
--- /dev/null
+++ b/site/versions/v0.7.0/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