![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Built with Rust](https://img.shields.io/badge/built%20with-Rust-orange.svg)
![GitHub stars](https://img.shields.io/github/stars/railyarddev/railyard)
![GitHub last commit](https://img.shields.io/github/last-commit/railyarddev/railyard)
![Tests](https://img.shields.io/badge/tests-74%20passed-brightgreen)

# railyard

**A Rust binary that sits between Claude Code and your system. Every tool call passes through Railyard before it touches the world.**

One install. 13 rules. <2ms decisions. Fully on-device.

```
$ railyard install
  ✓ Hooks registered with Claude Code
  ✓ Default protections active (13 rules)

$ claude "clean up duplicate AWS resources"
  ⛔ BLOCKED  terraform destroy
     rule: terraform-destroy · railyard.yaml:14
```

---

## Why

AI coding agents have shell access. They make mistakes — and prompt-based guardrails are just suggestions the model can ignore.

| Date | What happened |
|------|---------------|
| Feb 2026 | Claude Code ran `terraform destroy` on production. **1.9M rows gone.** |
| Feb 2026 | A background agent ran `drizzle-kit push --force`. **60 tables wiped.** |
| 2025-26 | Agents ran `rm -rf ~/`, `git reset --hard`, `DROP DATABASE` on live systems. |

These aren't hypothetical. Agents have been [documented bypassing their own safety rules](https://github.com/anthropics/claude-code/issues/29691). Railyard enforces rules **deterministically at the system level** — outside the LLM, where the model can't talk its way around them.

---

## How It Works

```
┌─────────────────────────────────────────────────────────┐
│  Claude Code                                            │
│                                                         │
│  Agent wants to run: terraform destroy                  │
│       │                                                 │
│       ▼                                                 │
│  PreToolUse hook fires ─────────────────────────────────┼──► railyard hook
│       │                                                 │         │
│       │  stdin: {                                       │         ▼
│       │    tool_name: "Bash",                           │    ┌─────────┐
│       │    tool_input: { command: "terraform destroy" } │    │ Railyard│
│       │  }                                              │    │ Binary  │
│       │                                                 │    └────┬────┘
│       │                                                 │         │
│       │                                                 │    ┌────▼────────────┐
│       │                                                 │    │ 1. Path Fence   │
│       │                                                 │    │ 2. Evasion      │
│       │                                                 │    │    Normalize     │
│       │                                                 │    │ 3. Policy Match  │
│       │                                                 │    │ 4. Snapshot      │
│       │                                                 │    │ 5. Trace Log     │
│       │                                                 │    └────┬────────────┘
│       │         ◄────── { permissionDecision: "deny" }  │         │
│       │                                                 │    exit 0
│       ▼                                                 │
│  ⛔ Tool blocked. Agent informed.                       │
│                                                         │
└─────────────────────────────────────────────────────────┘
```

Railyard hooks into Claude Code's native hooks system. Every tool call — shell commands, file writes, file reads — is intercepted **before execution**. The binary evaluates your policy in <2ms and returns allow, deny, or ask. No network calls. No latency. No way for the model to bypass it.

---

## Install

```bash
# Build from source
cargo install --path .

# Register hooks with Claude Code
railyard install
```

That's it. `terraform destroy`, `rm -rf /`, `DROP TABLE`, `git push --force`, and 9 other destructive patterns are now blocked out of the box. Zero config required.

---

## Features

### 1. Block — Stops Real Disasters

```
┌──────────────────────┐     ┌──────────────────┐     ┌─────────┐
│ Agent: "terraform     │────▶│ Evasion Engine   │────▶│ Policy  │
│         destroy"      │     │                  │     │ Engine  │
└──────────────────────┘     │ • Normalize      │     │         │
                              │ • Decode base64  │     │ blocklist│──▶ ⛔ DENY
┌──────────────────────┐     │ • Expand vars    │     │ approve │──▶ ⚠️ ASK
│ Agent: "echo b64.. | │────▶│ • Unwrap sh -c   │     │ allowlist│──▶ ✅ ALLOW
│   base64 -d | sh"    │     │ • Catch eval     │     └─────────┘
└──────────────────────┘     └──────────────────┘
```

Agents sometimes bypass safety by encoding commands in base64, wrapping them in `sh -c`, using variable expansion, or concatenating strings through `eval`. Railyard normalizes all of these before matching — so the evasion gets caught just like the original command:

- Direct: `terraform destroy` → blocked
- Base64: `echo dGVycmFmb3Jt... | base64 -d | sh` → decoded → blocked
- Variable: `CMD="terraform destroy"; $CMD` → expanded → blocked
- Shell wrap: `sh -c "terraform destroy"` → unwrapped → blocked
- Eval concat: `eval "ter""raform destroy"` → concatenated → blocked
- Backtick: `` `echo terraform` destroy `` → substituted → blocked
- Hex escape: `$'\x74\x65\x72\x72...'` → decoded → blocked

**Default blocklist (active with zero config):**

| Rule | Pattern | Source |
|------|---------|--------|
| `terraform-destroy` | `terraform destroy\|apply -auto-approve` | DataTalks incident |
| `rm-rf-critical` | `rm -rf` on `/`, `~/`, `$HOME` | GitHub issues |
| `sql-drop` | `DROP TABLE\|DATABASE\|SCHEMA` | Multiple incidents |
| `git-force-push` | `git push --force` | Common footgun |
| `git-reset-hard` | `git reset --hard` | Destroys uncommitted work |
| `git-clean-force` | `git clean -f` | Removes untracked files |
| `drizzle-force` | `drizzle-kit push --force` | Issue #27063 |
| `disk-format` | `mkfs`, `dd of=/dev/` | Disk destruction |
| `k8s-delete-ns` | `kubectl delete namespace` | K8s footgun |
| `aws-s3-rm-recursive` | `aws s3 rm --recursive` | Data loss |
| `docker-system-prune` | `docker system prune -a` | Removes all images |
| `chmod-777-recursive` | `chmod -R 777 /` | Security disaster |
| `npm-publish` | `npm publish` | Requires approval |
| `railyard-uninstall` | `railyard uninstall` | Self-protection |
| `railyard-tamper-settings` | `.claude/settings.json` | Self-protection |
| `railyard-remove-binary` | `rm .cargo/bin/railyard` | Self-protection |

### 2. Snapshot — Cursor-Style Checkpoints for Claude Code

```
  Edit 1          Edit 2          Edit 3          Edit 4 (bad)
    │               │               │               │
    ▼               ▼               ▼               ▼
┌────────┐     ┌────────┐     ┌────────┐     ┌────────┐
│snapshot│     │snapshot│     │snapshot│     │  (now)  │
│  #a1   │     │  #b2   │     │  #c3   │     │        │
└────────┘     └────────┘     └────────┘     └────────┘

$ railyard rollback --steps 1 --session abc123
  ✓ Restored src/main.rs from snapshot c3

$ railyard rollback --session abc123
  ✓ Restored src/main.rs from snapshot a1    ← back to original
  ✓ Restored src/lib.rs from snapshot a2
```

Every `Write` and `Edit` tool call captures a SHA-256 snapshot of the file **before** the change. Step backwards one edit at a time, jump to any snapshot, or rollback an entire session to its starting state.

**No git required.** Snapshots are independent of your commit history — they capture every intermediate state the agent creates, including new files (rollback = delete).

### 3. Trace — Know Exactly What the Agent Did

```
$ railyard log --session abc123

[2026-03-09T14:22:01Z] BLOCKED     Bash | PreToolUse: terraform destroy (terraform-destroy)
[2026-03-09T14:22:01Z]   OK        Bash | PreToolUse: npm test
[2026-03-09T14:22:02Z]   OK        Edit | PreToolUse: src/main.rs
[2026-03-09T14:22:02Z]   OK        Edit | PostToolUse: src/main.rs
[2026-03-09T14:22:03Z] APPROVE     Bash | PreToolUse: psql -h prod-db (prod-db)
```

Every tool call is logged as structured JSONL to `.railyard/traces/<session>.jsonl`:

```json
{
  "timestamp": "2026-03-09T14:22:01.003Z",
  "session_id": "abc123",
  "event": "PreToolUse",
  "tool": "Bash",
  "input_summary": "terraform destroy",
  "decision": "block",
  "rule": "terraform-destroy",
  "duration_ms": 1
}
```

Pipe to your observability stack. Grep for `"decision":"block"`. Full accountability for every action the agent took.

### 4. Path Fence — Prevent Credential Exfiltration

```
┌─────────────────────────────────────────────┐
│  Your Machine                               │
│                                             │
│  ┌─────────────────────┐                    │
│  │ /project (cwd)      │ ← ✅ Agent can    │
│  │   src/               │    read/write     │
│  │   tests/             │    here           │
│  │   railyard.yaml      │                   │
│  └─────────────────────┘                    │
│                                             │
│  ~/.ssh/                  ← ⛔ DENIED       │
│  ~/.aws/                  ← ⛔ DENIED       │
│  ~/.gnupg/                ← ⛔ DENIED       │
│  ~/.config/gcloud/        ← ⛔ DENIED       │
│  /etc/                    ← ⛔ DENIED       │
│                                             │
└─────────────────────────────────────────────┘
```

Agents can't touch `~/.ssh`, `~/.aws`, `~/.gnupg`, or `/etc/`. Your credentials stay on your machine, not in a model's context window.

### 5. Chat — Write Policies in English

```bash
$ railyard chat
  Launching interactive policy configuration...

# Describe what you want in plain English:
> "Block all database migrations against production"
> "Require approval for any docker push command"
> "Allow terraform plan but block terraform apply"

# Railyard generates the YAML rules for you.
```

Uses Claude Code under the hood to translate your intent into valid regex rules. No regex knowledge needed — just say what you want blocked.

---

## Configuration

```bash
# Generate a starter config
railyard init
```

This creates `railyard.yaml` in your project root:

```yaml
version: 1

blocklist:
  - name: terraform-destroy
    tool: Bash
    pattern: "terraform\\s+(destroy|apply\\s+.*-auto-approve)"
    action: block
    message: "Blocked: destructive infrastructure command"

approve:
  - name: npm-publish
    tool: Bash
    pattern: "npm\\s+publish"
    action: approve
    message: "npm publish requires approval"

allowlist: []

fence:
  enabled: true
  allowed_paths: []          # empty = project directory only
  denied_paths:
    - "~/.ssh"
    - "~/.aws"
    - "~/.gnupg"
    - "/etc"

trace:
  enabled: true
  directory: .railyard/traces

snapshot:
  enabled: true
  tools: [Write, Edit]
  directory: .railyard/snapshots
```

**Rule evaluation order:** allowlist → blocklist → approve → allow

---

## CLI Reference

```
railyard install              # Register hooks with Claude Code
railyard uninstall            # Remove hooks
railyard init                 # Generate starter railyard.yaml
railyard status               # Show current protection status
railyard chat                 # Interactive policy builder

railyard log                  # List sessions with traces
railyard log --session <id>   # View traces for a session

railyard rollback --session <id>              # List snapshots
railyard rollback --session <id> --steps 3    # Undo last 3 edits
railyard rollback --session <id> --id <snap>  # Restore specific snapshot
railyard rollback --session <id> --file path  # Restore specific file
```

---

## Testing

```bash
# Unit tests (46) + attack simulations (28) = 74 tests
cargo test

# Just the attack simulations
cargo test --test attack_simulation
```

Attack simulations reproduce every real-world incident:
- `incident_terraform_destroy_datatalks` — the DataTalks.Club 1.9M row wipe
- `incident_drizzle_kit_force_push` — the GitHub #27063 60-table drop
- `incident_rm_rf_home` — home directory deletion
- `evasion_base64_*` — base64 encoded commands
- `evasion_variable_expansion` — `$CMD` trick
- `evasion_shell_wrapper` — `sh -c` indirection
- `evasion_eval_concat` — string concatenation
- `fence_write_to_ssh_keys` — credential file access
- `self_protect_block_uninstall` — agent can't disable Railyard
- `self_protect_block_settings_edit` — agent can't edit hook config
- `self_protect_block_remove_binary` — agent can't delete the binary
- And 17 more.

---

## Architecture

```
railyard/
├── src/
│   ├── main.rs              # CLI (install, init, log, rollback, status, chat)
│   ├── types.rs             # Shared types (HookInput, Policy, Decision)
│   ├── hook/
│   │   ├── handler.rs       # Reads stdin JSON, dispatches to handlers
│   │   ├── pre_tool.rs      # PreToolUse: fence → policy → snapshot → trace
│   │   ├── post_tool.rs     # PostToolUse: trace completion
│   │   └── session.rs       # SessionStart: init logging
│   ├── block/
│   │   ├── matcher.rs       # Regex matching against rules
│   │   └── evasion.rs       # Command normalization (7 evasion techniques)
│   ├── policy/
│   │   ├── engine.rs        # Rule evaluation (allowlist → block → approve)
│   │   ├── loader.rs        # Find & parse railyard.yaml, merge defaults
│   │   └── defaults.rs      # Built-in blocklist (13 rules)
│   ├── fence/
│   │   └── path.rs          # Filesystem sandboxing
│   ├── snapshot/
│   │   ├── capture.rs       # SHA-256 snapshots before writes
│   │   └── rollback.rs      # Restore from any snapshot
│   ├── trace/
│   │   └── logger.rs        # JSONL structured logging
│   └── install/
│       └── hooks.rs         # Register/remove Claude Code hooks
├── defaults/
│   └── railyard.yaml        # Starter policy template
└── tests/
    └── attack_simulation.rs # 24 real-world incident reproductions
```

---

## Data Storage

```
~/.claude/settings.json       # Where hooks are registered
<project>/railyard.yaml       # Your policy (version controlled)
<project>/.railyard/          # Runtime data (gitignored)
├── traces/
│   └── <session>.jsonl       # Audit logs
└── snapshots/
    └── <session>/
        ├── manifest.jsonl    # Snapshot index
        └── <hash>.snapshot   # File content backups
```

---

## License

MIT License. Copyright 2026 Ari Choudhury.
