# Chisel
[](https://github.com/ckanthony/Chisel/actions/workflows/ci.yml)
[](https://codecov.io/gh/ckanthony/Chisel)

๐ช Rust powered precision file operations for agents. Unix-native tools, minimal context footprint, strict path confinement: use directly with Chisel MCP or bring your own MCP, embeddable in any MCP server in Rust, Python, Nodejs.
https://github.com/user-attachments/assets/af84f1af-db47-4e42-808b-00861504cd34
**Install** โ download a pre-built [`.mcpb` bundle](https://github.com/modelcontextprotocol/mcpb) (one-click, no build step) or a raw binary from the [Releases page](https://github.com/ckanthony/Chisel/releases/latest) โ see [Standalone usage](#standalone-usage) below.
---
> **Agent skill included** โ [`skills/chisel/SKILL.md`](skills/chisel/SKILL.md) teaches agents how to use Chisel at maximum efficiency. Install with: `npx skills add ckanthony/Chisel`
> **Security hardened** โ Verified properties across two layers: the MCP server (`chisel`) and the portable core library (`chisel-core`). See the [Security model](#security-model) section for the full breakdown.
---
## Contents
- [Motivation](#motivation)
- [Tools](#tools)
- [Standalone usage](#standalone-usage)
- [Binary](#binary)
- [Docker](#docker)
- [Configure your MCP client](#configure-your-mcp-client)
- [Behind a reverse proxy](#behind-a-reverse-proxy-caddy)
- [Bring your own MCP](#bring-your-own-mcp)
- [Extending chisel](#extending-chisel)
- [Workspace layout](#workspace-layout)
- [Security model](#security-model)
- [Platform support](#platform-support)
- [Development](#development)
- [Contributing](#contributing)
- [Future considerations](#future-considerations)
---
## Motivation
Most MCP file tools hand an LLM a blank canvas: read anything, write anything, make any mistake. Chisel takes the opposite approach:
- **Reduce context overhead** โ every tool call is compact. File edits go through `patch_apply`: the model sends only a unified diff instead of rewriting an entire file, so large-file edits cost a fraction of the tokens
- **Familiar command patterns** โ `shell_exec` exposes the same Unix tools (`grep`, `sed`, `awk`, `find`, `cat`, โฆ) LLMs already know well from training data, so prompts stay short and outputs are predictable
- **Precision over flexibility** โ a fixed whitelist and strict path confinement mean the model cannot accidentally escape scope or run arbitrary commands
- **Safety first** โ bearer-token auth, `127.0.0.1`-only binding, symlink-aware root confinement, atomic writes, and a read-only mode are all on by default
- **Reusable core** โ `chisel-core` is a plain synchronous Rust library; any MCP server (Rust, Node.js via WASM, Python via WASM) can embed it without running a second process
### Real-world demo
Six tasks on the same markdown file. Left: typical MCP file server. Right: chisel.
**File:** `docs/api.md` โ 300 lines, 6 headers, one section of ~20 lines.
---
**Task 1 โ find all headers**
```
# Typical # Chisel
tool: read_file tool: shell_exec
path: /data/docs/api.md command: grep
args: ["-n", "^#", "/data/docs/api.md"]
โ 300 lines returned (~3 000 tokens) โ 6 lines returned (~30 tokens)
model must scan the whole file 1:# API Reference
45:## Authentication
89:## Endpoints
134:## Request Format
178:## Response Format
234:## Errors
```
**Task 2 โ read the content under `## Endpoints`**
```
# Typical # Chisel
tool: read_file (again, or re-use above) tool: shell_exec
path: /data/docs/api.md command: sed
args: ["-n", "/^## Endpoints/,/^## /p",
โ 300 lines returned again (~3 000 tokens) "/data/docs/api.md"]
model must locate the section in context
โ 44 lines returned (~440 tokens)
only the Endpoints section
```
**Task 3 โ edit one line in that section**
```
# Typical # Chisel
tool: write_file tool: patch_apply
path: /data/docs/api.md path: /data/docs/api.md
content: --- a/docs/api.md
+++ b/docs/api.md
โ 300 lines uploaded (~3 000 tokens) @@ -91,1 +91,1 @@
any hallucination corrupts the file -GET /v1/items
+GET /v2/items
โ 7 lines uploaded (~50 tokens)
hunk mismatch โ PatchFailed, file untouched
```
**Task 4 โ replace all `-` with `:` in that section**
```
# Typical # Chisel
tool: read_file tool: shell_exec
path: /data/docs/api.md command: sed
args: ["-i", "s/-/:/g",
โ 300 lines returned (~3 000 tokens) "/data/docs/api.md"]
tool: write_file
content: <300 lines with replacement> โ 1 line call, 0 file content transmitted
โ 300 lines uploaded (~3 000 tokens) sed runs the replacement in-place
Total: ~12 000 tokens Total: ~520 tokens (23ร less)
```
**Task 5 โ model tries to run `rm -rf ~`**
```
# Typical # Chisel
(no shell tool exposed) tool: shell_exec
command: rm
args: ["-rf", "~"]
not applicable โ typical MCP file
servers have no shell tool, so the โ CommandNotAllowed
model would need a separate shell "rm" is not in the whitelist.
MCP or use write_file to script it Permitted: grep sed awk find cat
head tail wc sort uniq cut tr
diff file stat ls du rg
process is never spawned
```
> `rm`, `bash`, `sh`, `curl`, `chmod`, `sudo` โ none of these are in the whitelist.
> The list is fixed at compile time; it cannot be extended at runtime by the model.
**Task 6 โ model tries to edit `/Users/home/.ssh/config` directly**
```
# Typical # Chisel
tool: write_file tool: patch_apply
path: /Users/home/jor/.ssh/config path: /Users/home/jor/.ssh/config
content: patch:
โ succeeds if the server process โ OutsideRoot
has filesystem access โ no path resolved path /Users/home/.ssh/config
confinement in a naive file server does not start with root /data
I/O is never performed
```
> Every path โ including those passed to `shell_exec` โ is validated against the
> configured root before any I/O or process spawn. An absolute path outside root
> is always rejected, regardless of which tool is called.
---
### Context cost comparison
Estimates use **Claude Sonnet 4.6** tokenisation: typical source code averages **~10 tokens/line** (identifiers, punctuation, and whitespace each count as tokens under BPE).
**Single edit โ 500-line file, 5 lines changed:**
| | Naive (`read_file` โ `write_file`) | Chisel (`patch_apply`) | Reduction |
| ------------------------- | ----------------------------------------- | ---------------------------------- | --------- |
| Tokens in (upload) | ~5 000 (full file) | ~120 (11 diff lines + headers) | **42ร** |
| Tokens out (model output) | ~5 000 (full file rewrite) | ~15 (success ack) | **333ร** |
| **Round-trip total** | **~10 000** | **~135** | **~74ร** |
| Failure mode | Silent hallucination corrupts entire file | `PatchFailed` โ original untouched | โ |
**Read / search โ 2 000-line file:**
| Task | Naive (`read_file` full) | Chisel (`shell_exec`) | Reduction |
| ------------------- | -------------------------- | --------------------------------- | ---------- |
| Find a symbol | ~20 000 tokens (full read) | ~40 tokens (`grep` matched lines) | **500ร** |
| Count occurrences | ~20 000 tokens | ~5 tokens (`grep -c` integer) | **4 000ร** |
| Extract lines 40โ60 | ~20 000 tokens | ~210 tokens (`sed -n '40,60p'`) | **95ร** |
| Directory tree | ~20 000 tokens | ~300 tokens (`find` / `ls -R`) | **67ร** |
> Savings scale linearly with file size. A 2 000-line file costs 4ร more than the 500-line baseline above.
---
## Tools
Every path argument is canonicalized and confined to the configured root before any I/O โ `..` traversal and symlink escapes are rejected. This confinement is enforced inside `chisel-core` and applies equally when the library is embedded directly.
When using the MCP server (`chisel`), all tools additionally require `Authorization: Bearer `.
| Tool | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `patch_apply` | Apply a unified diff atomically; accepts raw or ````diff` fenced patches โ **primary edit tool; sends only changed lines, not the full file** |
| `append` | Append content to an existing file |
| `write_file` | Write (create or overwrite) a file; creates parent dirs automatically |
| `create_directory` | Create a directory tree (`mkdir -p` semantics) |
| `move_file` | Move or rename a file within root |
| `shell_exec` | Run a whitelisted command โ `grep sed awk find cat head tail wc sort uniq cut tr diff file stat ls du rg` |
Full reference: [docs/tools.md](docs/tools.md)
---
## Bring your own MCP
Chisel ships two embeddable libraries alongside the standalone server. Neither carries any HTTP, MCP protocol, or async runtime dependency โ drop them into your own server and own the transport entirely.
| Package | What it is | Use it when |
|---|---|---|
| [`chisel-core`](chisel-core/README.md) | Pure Rust sync library โ path confinement, all file ops, shell exec | Writing a Rust MCP server |
| [`chisel-wasm`](chisel-wasm/) | `chisel-core` compiled to `wasm32-wasip1` | Writing an MCP server in Node.js, Python, Deno, or any WASI runtime |
Full integration guide โ **[chisel-core/README.md](chisel-core/README.md)**
---
## Standalone usage
### Binary
**Option A โ Download pre-built binary (recommended)**
Go to the [latest release](https://github.com/ckanthony/Chisel/releases/latest) and download the binary for your platform:
| Platform | File |
|---|---|
| macOS Apple Silicon | `chisel-macos-arm64` |
| macOS Intel | `chisel-macos-x86_64` |
| Linux x86-64 | `chisel-linux-x86_64` |
| Linux ARM64 | `chisel-linux-arm64` |
```bash
# Make executable and run
chmod +x chisel-macos-arm64 # adjust filename for your platform
MCP_APP_SECRET=mysecret ./chisel-macos-arm64 --root /path/to/data
```
**Option B โ Build from source**
```bash
cargo build --release -p chisel
```
**Running**
```bash
# Secret via env var (preferred)
MCP_APP_SECRET=mysecret ./chisel --root /path/to/data
# Or via --secret flag (env var takes precedence if both set)
./chisel --root /path/to/data --secret mysecret
# Read-only mode (shell_exec still works; writes are blocked)
MCP_APP_SECRET=mysecret ./chisel --root /path/to/data --read-only
```
The server binds to `127.0.0.1:3000` by default. Use `--port` to change the port.
### Docker
Three ways to run Chisel in Docker โ pick the one that fits your workflow.
#### Option 1 โ Docker Compose (recommended)
```bash
# 1. Create your .env
echo "MCP_APP_SECRET=changeme" > .env
# 2. Create the data directory that will be exposed to the LLM
mkdir -p data
# 3. Start (builds the image on first run, then stays running)
docker compose up -d
# Tail logs
docker compose logs -f
# Stop
docker compose down
```
`docker-compose.yml` mounts `./data` โ `/data` inside the container and binds `127.0.0.1:3000:3000` โ the port is never exposed beyond localhost.
#### Option 2 โ `docker run`
```bash
# Build
docker build -t chisel:latest .
# Run (replace /absolute/path/to/data with your actual data directory)
docker run -d \
--name chisel \
-e MCP_APP_SECRET=changeme \
-v /absolute/path/to/data:/data \
-p 127.0.0.1:3000:3000 \
--restart unless-stopped \
chisel:latest
# Read-only mode (writes blocked, shell_exec still works)
docker run -d \
--name chisel \
-e MCP_APP_SECRET=changeme \
-v /absolute/path/to/data:/data:ro \
-p 127.0.0.1:3000:3000 \
chisel:latest chisel --root /data --read-only
```
#### Option 3 โ Custom port
```bash
docker run -d \
--name chisel \
-e MCP_APP_SECRET=changeme \
-v /absolute/path/to/data:/data \
-p 127.0.0.1:8080:3000 \
chisel:latest
```
Host port `8080` maps to container port `3000`. Update your MCP client URL to `http://127.0.0.1:8080/mcp` accordingly.
> The container runs as a non-root user (`chisel`) and never binds on `0.0.0.0`. For remote access, place Caddy or nginx in front โ see [Behind a reverse proxy](#behind-a-reverse-proxy-caddy).
### Configure your MCP client
```json
{
"mcpServers": {
"chisel": {
"url": "http://127.0.0.1:3000/mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}
```
---
## Behind a reverse proxy (Caddy)
Chisel deliberately refuses to bind on `0.0.0.0`. Remote or multi-client access goes through a reverse proxy that handles TLS.
### Caddy โ automatic HTTPS
```caddy
mcp.yourdomain.com {
reverse_proxy 127.0.0.1:3000
}
```
Caddy auto-provisions a Let's Encrypt certificate. The bearer token still authenticates every request end-to-end.
```bash
# Install Caddy (macOS)
brew install caddy
# Run
caddy run --config Caddyfile
```
Your MCP client URL becomes `https://mcp.yourdomain.com/mcp`.
### nginx
```nginx
server {
listen 443 ssl;
server_name mcp.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
}
```
---
## Extending chisel
See **[chisel-core/README.md](chisel-core/README.md)** for the full integration guide covering:
- Rust API reference with code examples
- `spawn_blocking` pattern for async handlers
- Node.js (โฅ 22) WASM / WASI integration
- Python (`wasmtime-py`) WASM integration
- Error types and security properties
---
## Workspace layout
```
chisel/
โโโ chisel/ # HTTP server binary (MCP Streamable HTTP transport)
โโโ chisel-core/ # Portable sync library โ no HTTP, no async, no MCP protocol
โโโ chisel-wasm/ # wasm32-wasip1 build of chisel-core for Node.js / Python / Deno
```
```mermaid
graph TD
A[LLM / MCP client] -->|HTTP + Bearer| B[chisel binary]
B --> C[chisel-core]
D[Your Rust MCP server] --> C
E[Node.js MCP server] -->|WASI| F[chisel-wasm .wasm]
G[Python MCP server] -->|WASI| F
F --> C
```
---
## Security model
Security properties are split across two layers. Each row is verified by the test suite referenced at the bottom of this section.
### `chisel` โ MCP server / HTTP layer
| # | Property | Mechanism | Default |
| --- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | --------- |
| 1 | **Bearer-token auth** โ every request authenticated | `subtle::ConstantTimeEq`; timing-safe comparison; missing or empty secret โ process exits at startup | required |
| 2 | **Rate limiting** โ brute-force and runaway-agent protection | `governor` token-bucket; excess requests โ `HTTP 429` before auth is checked | 100 req/s |
| 3 | **Request body cap** โ memory exhaustion prevention | `axum::DefaultBodyLimit`; oversized body โ `HTTP 413` before any parsing | 4 MiB |
| 4 | **Audit log** โ every tool op traceable | `tracing::info` on success, `tracing::warn` on failure; records op name, path, error | `info` |
| 5 | **Loopback-only binding** โ no accidental public exposure | `TcpListener::bind("127.0.0.1:")`; `0.0.0.0` is never used | โ |
### `chisel-core` โ portable library (enforced on every op, including embedded use)
| # | Property | Mechanism |
| --- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 6 | **Kernel-enforced root confinement** โ directory traversal, symlink escape, TOCTOU all blocked | `cap_std::fs::Dir`; every path component traversed via `openat(fd, component, O_NOFOLLOW)`; confinement is enforced at the kernel level during I/O, not in userspace before it |
| 7 | **Atomic writes** โ failed patch never corrupts the target file | `Dir::create(".name.PID.tmp")` + `Dir::rename(tmp โ target)`; both ops confined inside the same root fd; on any failure the tmp file is discarded and the original is untouched |
| 8 | **Read-only mode** โ blanket write protection | `check_writable(read_only)` runs before any I/O inside every write op; no code path bypasses it |
| 9 | **Shell whitelist + direct `execve`** โ no injection, no arbitrary commands | Fixed compile-time whitelist; `std::process::Command` spawns directly (no `sh -c`); path-like args validated via `validate_path` before spawn |
```mermaid
graph LR
req[Incoming request] --> body{Body โค limit?\nDefaultBodyLimit}
body -- too large --> 413[HTTP 413]
body -- ok --> rate{Rate limit\ntoken-bucket}
rate -- exceeded --> 429[HTTP 429]
rate -- ok --> auth{Bearer token\nconstant-time compare}
auth -- reject --> 401[HTTP 401]
auth -- pass --> path{Path confinement\ncap-std openat + O_NOFOLLOW}
path -- outside root --> err[OutsideRoot error]
path -- inside root --> ro{Read-only mode?}
ro -- write op --> readonly[ReadOnly error]
ro -- read / shell --> exec[Execute tool]
exec --> audit[Audit log\ntracing::info/warn]
exec --> atomic[Atomic write\nDir::create + Dir::rename]
```
### Attack and misuse prevention
Each layer targets a specific class of failure โ accidental or deliberate.
#### 1. Authentication โ unauthorised access
Every HTTP request must carry `Authorization: Bearer `. The comparison uses `subtle::ConstantTimeEq`, which takes the same number of CPU cycles regardless of how many characters match. A timing-based brute-force attack that probes the token character-by-character cannot succeed because no timing signal leaks.
The server refuses to start if the secret is empty or absent โ misconfiguration is a hard error, not a silent fallback to no auth.
#### 2. Path confinement โ escape from root directory
This is the core guarantee. All filesystem tools (`patch_apply`, `append`, `write_file`, `create_directory`, `move_file`) operate exclusively through a `cap_std::fs::Dir` handle rooted at the configured root directory.
```
input string
โ strip root prefix โ relative path
โ cap_std::fs::Dir::open / write / rename โฆ
โโ kernel: openat(root_fd, "sub/file", O_NOFOLLOW)
openat(sub_fd, "file", O_NOFOLLOW)
```
Every component of the path is traversed via `openat` with `O_NOFOLLOW`. The kernel enforces confinement โ no userspace prefix check can be bypassed:
| Attack | Example input | What happens |
| -------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Directory traversal | `/data/sub/../../etc/passwd` | `..` components tracked per-fd; escape above root blocked by kernel โ error |
| Absolute path bypass | `/etc/hosts` | Root prefix stripped; remaining path confined to root fd โ `OutsideRoot` |
| Symlink in component | `/data/link/file` where `link โ /etc` | `O_NOFOLLOW` on `link` open โ kernel rejects โ error |
| TOCTOU swap | Valid path at check time, swapped to symlink before I/O | No separate check/use window โ confinement is enforced during the I/O itself |
`validate_path` (the userspace canonicalize + prefix check from earlier versions) is still used exclusively for `shell_exec` path arguments, where we pass strings to spawned processes that `cap-std` cannot confine.
#### 3. Shell injection โ arbitrary command execution
`shell_exec` does **not** invoke a shell interpreter. It calls `std::process::Command` directly with the command and arguments as separate OS-level strings:
```
shell_exec("grep", ["-r", "foo", "/data"])
โ execve("/usr/bin/grep", ["-r", "foo", "/data"]) โ no sh -c wrapper
```
Shell metacharacters (`;`, `&&`, `|`, `$()`, backticks, etc.) in any argument are passed as literal bytes to the target process. There is no shell to interpret them.
A fixed compile-time whitelist (`grep sed awk find cat head tail wc sort uniq cut tr diff file stat ls du rg`) is checked before the process is spawned. Any command not on the list returns `CommandNotAllowed` immediately โ the process is never started.
Path-like arguments (starting with `/` or containing `..`) are validated against root via `validate_path` before the process starts.
#### 4. Partial write โ file corruption on failed patch
`patch_apply` never writes directly to the target file. The flow is entirely confined within the `cap-std` Dir:
```
1. Parse and validate the diff
2. Dir::create(".filename.PID.tmp") โ confined temp file, same directory
3. On success โ Dir::rename(tmp, target) โ single syscall, cannot be interrupted mid-write
4. On failure โ return PatchFailed, tmp is dropped and cleaned up
```
If the hunk context does not match the current file (the file has drifted since the diff was generated), the operation aborts at step 1 and the original file is never touched. A process crash between steps 2 and 3 leaves the `.tmp` file โ the original is still intact.
#### 5. Read-only mode โ blanket write protection
Starting with `--read-only` causes all five write tools (`patch_apply`, `append`, `write_file`, `create_directory`, `move_file`) to return `ReadOnly` immediately without performing any I/O. The check happens inside `chisel-core` before any disk access โ there is no code path that bypasses it.
`shell_exec` remains available in read-only mode because it only reads (whitelisted commands are all inspection tools; `mkdir` and `mv` are explicitly excluded from the whitelist).
#### 6. Rate limiting โ brute-force and runaway-agent protection
Every request passes through a token-bucket rate limiter (`governor`) before authentication. When the configured rate (default: **100 req/s**) is exceeded, the server immediately returns `HTTP 429 Too Many Requests` without doing any work.
This prevents two threat classes:
- **Token brute-force**: an attacker cannot enumerate tokens faster than the configured rate, even with a leaked secret in partial form.
- **Runaway agent loops**: a malfunctioning LLM client flooding the server is throttled before it can cause resource exhaustion.
Configure with `--rate-limit ` (or set to `0` to disable). The limiter is the outermost layer โ requests that exceed the rate never reach the auth check.
#### 7. Request body cap โ memory exhaustion prevention
All incoming request bodies are capped at **4 MiB** by default (`DefaultBodyLimit`). An oversized body is rejected before any parsing, authentication, or tool dispatch occurs.
Configure with `--body-limit `. For typical LLM usage (unified diffs and file content), 4 MiB is generous โ increase only if you intentionally send large file writes.
#### 8. Audit logging โ operation traceability
Every tool invocation emits a structured `tracing` log line recording the operation name, the target path (or command), and whether it succeeded or failed:
```
INFO chisel::tools::filesystem op=patch_apply path=/data/foo.txt
WARN chisel::tools::filesystem op=write_file path=/etc/passwd error=OutsideRoot { ... }
INFO chisel::tools::shell op=shell_exec cmd=grep exit_code=0
```
Log verbosity is controlled via the `RUST_LOG` environment variable (e.g. `RUST_LOG=chisel=debug`). The default level is `info`, which captures every tool call result without flooding output with framework internals.
#### 9. Network exposure โ no accidental public binding
The server calls `TcpListener::bind("127.0.0.1:")` โ not `0.0.0.0`. It is impossible for the process itself to accept connections from outside the machine. Remote access must be deliberately routed through a reverse proxy (Caddy, nginx), which is where TLS termination and any additional access controls live.
The Docker image runs as a non-root user (`mcp`) and the `docker-compose.yml` maps the port as `127.0.0.1:3000:3000`, preserving the loopback restriction even inside a container.
### Security test coverage
Every property above is verified by a dedicated test suite at `[chisel/tests/security.rs](chisel/tests/security.rs)`.
Run with `cargo test --test security -p chisel`.
| Property | Tests |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **ยง1 Authentication** | `auth_missing_secret_is_hard_error` ยท `auth_empty_secret_is_hard_error` ยท `auth_missing_header_returns_401` ยท `auth_wrong_token_returns_401` ยท `auth_basic_scheme_returns_401` ยท `auth_prefix_of_secret_returns_401` ยท `auth_valid_token_passes` |
| **ยง2 Path confinement** | `path_directory_traversal_is_blocked` ยท `path_absolute_outside_root_returns_outside_root_error` ยท `path_symlink_in_component_is_blocked` ยท `path_toctou_symlink_swap_is_blocked` ยท `path_deeply_nested_outside_root_is_blocked` |
| **ยง3 Shell injection** | `shell_dangerous_commands_blocked_before_spawn` ยท `shell_metacharacters_are_literal` ยท `shell_path_arg_outside_root_blocked_before_spawn` ยท `shell_traversal_in_arg_blocked_before_spawn` |
| **ยง4 Partial write** | `partial_write_failed_patch_leaves_file_intact` ยท `partial_write_no_tmp_artefact_on_failure` |
| **ยง5 Read-only mode** | `readonly_all_write_tools_are_blocked` ยท `readonly_shell_exec_remains_available` ยท `readonly_no_disk_mutation_occurs` |
| **ยง6 Network binding** | `network_bind_address_is_loopback_only` ยท `network_sse_endpoint_returns_404` |
All 23 tests pass on every CI run. A failure means a documented security guarantee has regressed.
```
$ cargo test --test security -p chisel
running 23 tests
test auth_empty_secret_is_hard_error ... ok
test auth_missing_secret_is_hard_error ... ok
test network_bind_address_is_loopback_only ... ok
test path_absolute_outside_root_returns_outside_root_error ... ok
test path_deeply_nested_outside_root_is_blocked ... ok
test readonly_all_write_tools_are_blocked ... ok
test readonly_no_disk_mutation_occurs ... ok
test path_directory_traversal_is_blocked ... ok
test shell_dangerous_commands_blocked_before_spawn ... ok
test shell_path_arg_outside_root_blocked_before_spawn ... ok
test path_symlink_in_component_is_blocked ... ok
test shell_traversal_in_arg_blocked_before_spawn ... ok
test partial_write_failed_patch_leaves_file_intact ... ok
test partial_write_no_tmp_artefact_on_failure ... ok
test path_toctou_symlink_swap_is_blocked ... ok
test readonly_shell_exec_remains_available ... ok
test shell_metacharacters_are_literal ... ok
test auth_missing_header_returns_401 ... ok
test auth_valid_token_passes ... ok
test auth_basic_scheme_returns_401 ... ok
test auth_prefix_of_secret_returns_401 ... ok
test auth_wrong_token_returns_401 ... ok
test network_sse_endpoint_returns_404 ... ok
test result: ok. 23 passed; 0 failed
```
---
## Development
```bash
# Run all tests
cargo test --workspace
# Run only the server tests
cargo test -p chisel
# Build the WASM target
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 -p chisel-wasm
# Build Docker image
docker build -t chisel:dev .
```
---
## Platform support
| Platform | Server binary (`chisel`) | `chisel-core` (lib, no shell) | `chisel-wasm` |
| ---------------------------- | ------------------------ | ----------------------------- | ------------- |
| Linux (x86_64, arm64) | โ
Full support | โ
| โ
|
| macOS (Apple Silicon, Intel) | โ
Full support | โ
| โ
|
| Windows | โ Not supported | โ ๏ธ Compiles, untested | โ
|
**Linux / macOS** โ primary targets. All nine security properties hold. Docker image is Debian-based. `shell_exec` whitelist (`grep`, `sed`, `awk`, `find`, `cat`, `ls`, โฆ) assumes a standard Unix environment.
**Windows** โ not supported for the server binary for three reasons:
1. `shell_exec` whitelist is entirely Unix tools; most do not exist natively on Windows
2. `cap-std` kernel confinement uses POSIX `openat + O_NOFOLLOW` semantics; Win32 reparse-point / junction handling differs
3. Symlink creation requires elevated privileges (`SeCreateSymbolicLinkPrivilege`) โ security tests cannot run without Administrator or Developer Mode
`**chisel-wasm`** โ fully portable. WASM has no OS dependency; runs identically on any WASI-capable runtime (Node.js โฅ 22, Deno, Wasmtime) regardless of host OS. `shell_exec` is excluded from the WASM build.
---
## Contributing
Contributions are welcome. A few guidelines:
- **Bug fixes and small improvements** โ open a PR directly.
- **New tools or behaviour changes** โ open an issue first to agree on scope before writing code.
- **Security issues** โ do not open a public issue. File a private report and include reproduction steps.
Before submitting:
```bash
cargo test --workspace # all tests must pass
cargo test --test security -p chisel # security suite must be green
cargo clippy --workspace -- -D warnings
cargo fmt --check
```
New security-relevant code should come with a matching test in `chisel/tests/security.rs` that names the exact attack vector it covers.
---
## Future considerations
### S3 / R2 object storage backend
A potential extension is a thin sync layer that hydrates S3 or R2 objects into a local scratch directory (RAM-backed or `tmpfs`), runs all Chisel operations against that directory through the normal `chisel-core` path, then flushes changed objects back on completion.
The agent would interact exclusively with Chisel โ path confinement, atomic patching, and shell tooling all behave identically. S3/R2 would serve purely as the persistence layer, transparent to the model.
Open problems before this is viable: concurrent writes to the same object need optimistic locking (ETag-based check-and-swap on flush), and a native `cap-std` equivalent does not exist for object storage, so confinement must be enforced at the scratch directory level rather than the storage layer. The core `shell_exec` whitelist also assumes a Unix filesystem and would require the object to be fully materialized locally before any command runs.