Now for Claude Code · macOS & Linux · v0.2.2

Rules your coding agent can't switch off.

Handrail installs security and privacy policy for Claude Code where the agent itself cannot change it, and tells you plainly which rules are enforced and which are only requests.

$curl -fsSL https://handrail.bitey.ai/install.sh | sh

Then: handrail use baseline --dry-run to see exactly what would change, and handrail use baseline to apply it.

8 packs · 3 profiles Apache-2.0 No account, no telemetry Other ways to install
The problem

A rule in a prompt is a request. Handrail installs enforcement.

Most "agent rules" are lines in an instructions file. The model usually follows them — and the agent, running as you, can edit or delete the file. Nothing tells you which of your rules are real.

request

A line in an instructions file

# ~/.claude/CLAUDE.md
Never publish anything to the internet.
  • The model decides whether to follow it
  • Anything running as you can change the file
  • No way to tell if it was loaded
enforced

Policy the agent evaluates

# managed-settings.d/handrail-privacy.json  (owned by root)
{ "enableArtifact": false,
  "disableRemoteControl": true, … }
  • Claude Code applies it, whatever the model thinks
  • Top precedence: user and project settings cannot override it
  • Changing it needs your password — the agent can't
What you get

Built to be trusted with root.

Handrail writes system policy, so every change is planned, shown, checked and reversible.

Out of the agent's reach

Enforced packs live in Claude Code's root-owned managed policy directory, which user and project settings cannot override.

Honest about enforcement

Every pack says whether Claude Code fully enforces it or only partly, what it costs you, and what it cannot do.

Transactional

Changes are staged, backed up and journaled. If a run dies halfway, the next one restores the previous state exactly.

Trusts nothing it's handed

Only which packs you want goes to sudo. The privileged step recomputes the plan itself and refuses if it differs from the one you reviewed.

Detects tampering

handrail doctor compares installed files with what Handrail wrote and warns when another policy source would override yours.

Leaves no trace

handrail disable --all removes everything it installed. Files it didn't create are never touched, byte for byte.

Packs

Pick what you need. See what it costs.

enforced Claude Code applies it whatever the model does. partial Enforced for common paths; the pack's limits say what slips through.

enforcedenforced tieranti-bypass

No bypassing

Lock out 'skip all permission prompts' mode and stop a personal setting from switching off every hook. Without this, every other pack can be bypassed in one step.

Details
Protects against
  • Bypassing every prompt with --dangerously-skip-permissions
  • Disabling managed hooks with disableAllHooks in personal settings
Tradeoffs
  • bypassPermissions mode is unavailable (auto mode is not affected)
Limits
A local administrator can still edit the managed directory. Protecting against the administrator requires MDM.
handrail enable anti-bypass
partialenforced tierdestructive

Confirm destructive commands

Force-push, hard reset, recursive delete, git clean and similar irreversible commands always ask first. sudo is denied outright.

Details
Protects against
  • git push --force overwriting others' commits
  • git reset --hard / git clean discarding uncommitted work
  • Accidental rm -rf
  • Reusing a sudo credential you entered recently
Tradeoffs
  • These commands stop and ask even in auto mode
Limits
Rules match command prefixes. Wrapping rm in a script, an alias or a different spelling gets around them. This is a speed bump, not isolation.
handrail enable destructive
enforcedenforced tiersandbox

OS-level sandbox

Enforced by the operating system (macOS Seatbelt, Linux bubblewrap): Bash and every child process cannot read credential directories and can only reach allowlisted domains.

Details
Protects against
  • Any process, including Python or Node scripts, reading ~/.ssh, ~/.aws and similar
  • Sending data to arbitrary domains
Tradeoffs
  • Network commands can only reach allowlisted domains (package registries and GitHub by default); ssh to your own servers fails until you add them
  • If the sandbox cannot start, Claude Code refuses to run instead of silently falling back
  • A command that fails in the sandbox cannot be retried outside it
Limits
Commands you type yourself after the ! prefix are not sandboxed. Native Windows is not supported; use WSL2.
handrail enable sandbox
partialenforced tiersecrets

Credentials and secret files

Stop Claude's own file tools from reading or editing SSH keys, cloud credentials, .env files and private keys.

Details
Protects against
  • ~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, ~/.docker, ~/.netrc, cloud CLI credentials
  • .env, *.pem and *.key files in projects
  • macOS keychain files
Tradeoffs
  • You edit .env files yourself
  • Legitimate *.key files in a project are blocked too
Limits
Covers Claude's built-in file tools and the file commands it recognizes in Bash (cat, head, sed, ...). A Python script that opens a file itself is not covered. For OS-level blocking, add the sandbox pack.
handrail enable secrets
enforcedenforced tiersupply-chain

Run only your own hooks

Run only hooks from managed policy; hooks shipped inside cloned repositories never run. Also disables automatic installation from the official plugin marketplace.

Details
Protects against
  • When you trusted a parent folder or run claude -p, hooks defined in a cloned repository's .claude/settings.json run arbitrary commands
Tradeoffs
  • Hooks in your own ~/.claude/settings.json stop running too (terminal integrations, custom notifications)
  • To keep one of your hooks, package it as a pack of your own
Limits
Does not cover MCP servers or skills shipped in a repository. Inspect an unfamiliar repository's .claude/ directory before working in it.
handrail enable supply-chain
enforcedenforced tierprivacy

Keep data on this machine

Turn off every optional upload: Remote Control, cloud agents, hosted artifacts, claude.ai connectors, telemetry, error reports, feedback and transcript uploads.

Details
Protects against
  • Sessions synced to the cloud
  • Output published to claude.ai
  • Feedback reports that attach the whole conversation and code
  • Telemetry and error reports
Tradeoffs
  • No Remote Control from your phone
  • The Artifact tool and claude.ai connectors (Gmail, Drive, ...) are unavailable
  • /feedback, /bug and /share are unavailable
Limits
Model inference itself must send the conversation to the model API. No local setting can change that. See the README section 'Limits'.
Requires
Claude Code 2.1.242 or later
handrail enable privacy
enforcedenforced tierretention

Keep local transcripts 7 days

Session transcripts are stored in plaintext under ~/.claude/projects/ for 30 days by default. Reduce that to 7 days.

Details
Protects against
  • Passwords and customer data that appeared in a conversation lingering on disk
  • Exposure if the machine is lost or someone else logs in
Tradeoffs
  • Sessions older than 7 days can no longer be resumed
Limits
Does not delete copies already taken by backups or cloud sync. Also enable full-disk encryption and exclude ~/.claude from cloud sync.
handrail enable retention
enforcedenforced tieraudit

Local audit log

Every tool call leaves one line on this machine: when, which session, which directory, what was called. Written locally, never sent anywhere.

Details
Protects against
  • Answering 'what exactly did it do?' after the fact
  • Spotting unexpected network access or deletions
Tradeoffs
  • The log can contain sensitive command-line arguments (common password patterns are masked); files are mode 600
Limits
The log is written as you, so any process running as you can edit it. It protects against mistakes, not deliberate tampering.
handrail enable audit
Profiles

Or start from a profile.

A profile is a named set of packs. handrail use <profile> makes the installed set exactly that; your own rules are kept.

baseline recommended

Barely changes daily use. Recommended for everyone.

privacyanti-bypassaudit
handrail use baseline
strict

Baseline plus credential protection, confirmation for destructive commands, shorter local retention.

privacyanti-bypasssecretsdestructiveauditretention
handrail use strict
paranoid

Adds the OS-level sandbox and runs only your own hooks. Changes daily use; read each pack's tradeoffs first.

privacyanti-bypasssecretsdestructivesupply-chainauditretentionsandbox
handrail use paranoid
How it works

Handrail writes the policy. Claude Code enforces it.

Handrail doesn't sit between you and the agent. It puts policy where Claude Code reads it first — and nothing else can override it.

$ handrail use baseline
  sudo receives only: which packs you want

/Library/Application Support/ClaudeCode/   # root-owned
├─ managed-settings.d/
│  ├─ handrail-privacy.json      # settings & permission rules
│  ├─ handrail-anti-bypass.json
│  └─ handrail-audit.json
├─ handrail/packs/privacy/hooks/
│  └─ guard.sh                   # runs before every tool call
└─ CLAUDE.md                     # Handrail's block: loaded
                                 # in every session
  1. Plan

    Handrail computes the exact files a change writes. --dry-run shows them; nothing else is touched.

  2. Elevate — the intent only

    You confirm and type your password. The privileged step rebuilds the plan from the catalog inside the binary and checks it matches.

  3. Apply transactionally

    Validate, stage, back up, journal, commit. Interrupted runs roll back to the exact previous state.

  4. Claude Code enforces

    Settings, permission rules and hooks are applied by Claude Code itself; the instructions block is guidance for the model.

See the source

In a new claude session run /status: the managed policy appears under "Setting sources". claude doctor shows the same.

See the instructions

Run /memory: the managed CLAUDE.md with Handrail's block is listed as loaded.

Watch it block

Ask for an Artifact, or start with --dangerously-skip-permissions: both are refused. With audit, every tool call is logged locally.

Check for tampering

handrail doctor flags edited or missing files, a writable binary, and MDM policy that would override Handrail.

Extend

Your rules, your packs, shared in one command.

Security is the first category, not the last. A pack is a directory of plain files; the same checks that guard the built-in catalog run on yours.

  1. Add a rule of your own

    The quickest extension: one sentence, placed in the root-owned instructions block that every session loads. Good for team conventions that don't need enforcement.

    $ handrail rule add "Reply in English"                  # id: rule-7k3m
    $ handrail rule add "Keep commit subjects under 50 characters" --id commits
    $ handrail rule list
    $ handrail rule edit commits "Commit subjects: 50 characters, imperative mood"
    $ handrail rule remove rule-7k3m
  2. Write a pack

    When a rule needs teeth, make it a pack. This complete example keeps agents away from production: a permission rule that asks before deploys, a hook that blocks commands aimed at production, instructions for the model, and tests for the hook.

    my-packs/
    └─ packs/no-prod/
       ├─ pack.toml                  # what it is, what it costs, what it can't do
       ├─ rules.md                   # instructions loaded into every session
       ├─ claude-code/
       │  ├─ settings.json           # permissions and hooks, enforced by Claude Code
       │  └─ hooks/guard.sh          # exit 2 blocks the tool call
       └─ tests/guard.cases          # inputs the hook must block or allow
    packs/no-prod/pack.toml
    id = "no-prod"
    version = "1.0.0"
    category = "team"
    tier = "enforced"
    title = "Keep agents away from production"
    summary = "Commands aimed at production hosts or a production environment are blocked; deploy scripts ask first."
    protects = [
      "A migration or delete run against the production database",
      "Deploying from a laptop by accident",
    ]
    tradeoffs = [
      "Legitimate production work has to happen outside the agent",
    ]
    limits = "Matches command text. A script that reads the host from a config file gets past it."
    
    [targets.claude-code]
    enforcement = "partial"
    settings = "claude-code/settings.json"
    hooks = ["claude-code/hooks/guard.sh"]
    packs/no-prod/rules.md
    ### Production (no-prod)
    
    - Never run commands against production hosts, databases or environments.
    - When a task needs production access, write the command out for the user to run.
    packs/no-prod/claude-code/settings.json
    {
      "permissions": {
        "ask": ["Bash(./deploy*)", "Bash(kubectl --context prod*)"]
      },
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [{ "type": "command", "command": "\"@PACK_DIR@/hooks/guard.sh\"" }]
          }
        ]
      }
    }
    packs/no-prod/claude-code/hooks/guard.sh
    #!/bin/sh
    # Exit 2 blocks the call; stderr tells the model why.
    input=$(cat)
    if printf '%s' "$input" | grep -Eq 'prod\.internal|--env[= ]production|RAILS_ENV=production'; then
      echo "Blocked by no-prod: this command targets production" >&2
      exit 2
    fi
    exit 0
    packs/no-prod/tests/guard.cases — tab-separated: name, expected exit code, hook input
    # name	expected exit	input
    production host	2	{"tool_name":"Bash","tool_input":{"command":"psql -h db.prod.internal"}}
    production env	2	{"tool_name":"Bash","tool_input":{"command":"rails db:migrate RAILS_ENV=production"}}
    local database	0	{"tool_name":"Bash","tool_input":{"command":"psql -h localhost"}}

    @PACK_DIR@ becomes the pack's installed hook directory. Hooks run as /bin/sh on every matching call, so keep them small: a broken managed hook blocks every session. The pack format reference lists every field.

  3. Check and try it locally

    handrail check validates the format, runs every hook test and lints the scripts. --catalog puts your directory next to the built-in packs for any command, so you can review the exact files before anything is written.

    $ handrail check ./my-packs
    $ handrail show no-prod --catalog ./my-packs
    $ handrail enable no-prod --catalog ./my-packs --dry-run
    $ handrail enable no-prod --catalog ./my-packs

    Handrail installs a copy of the pack, so it keeps working if you move or delete the directory; run enable again with --catalog to install a new version. An external pack can't take the id of a built-in pack or override its settings.

  4. Share it with a link

    Push the directory to any git repository. Anyone who trusts it installs your pack straight from the repository; no review, no waiting. Private repositories work with your usual git credentials, which makes this the simplest way to roll a policy out to a team.

    # you
    $ git -C my-packs init && git -C my-packs add . && git -C my-packs commit -m "no-prod"
    $ git -C my-packs push git@github.com:you/my-packs.git main
    # your teammates
    $ handrail enable no-prod --catalog github.com/you/my-packs
  5. Publish it to the shared catalog

    Think others would want it? handrail publish proposes it to handrail-packs as a pull request. It runs the checks, forks the catalog into your account if needed, commits the pack on a branch and opens the pull request, all through the GitHub API. You don't need git or a manual fork.

    $ handrail publish no-prod --catalog ./my-packs --dry-run
    $ handrail publish no-prod --catalog ./my-packs
    • Access: uses the GitHub CLI if it's signed in, or a token in GITHUB_TOKEN (scope public_repo). With neither, it prints the manual steps.
    • Review: packs can run as root on other people's machines, so a maintainer reviews every pull request. Once merged, the pack ships in the next Handrail release and anyone can install it by name.
    • Updates: bump version and publish again; files you removed are removed in the pull request too.
    • Your own catalog: --repo your-org/policy-packs sends the pull request to a team repository instead.
  6. Bring another agent

    Packs describe intent. Each agent gets an adapter that maps that intent to the strongest mechanism the agent offers, and labels it honestly when the result is only a suggestion. A pack adds support by adding a directory next to claude-code/ and a [targets.<agent>] section.

    See the roadmap →

Limits

What Handrail can't do.

A security tool that overclaims is worse than none. So, plainly:

Inference still leaves the machine

Your conversation is sent to the model API — that's how the model answers. Handrail turns off every optional upload and cloud path, not that one.

Administrators can undo it

A local admin can edit the managed directory. Protecting against the admin needs MDM.

MDM policy wins

If your organisation deploys Claude Code policy by MDM, Claude Code ignores file-based policy by default. handrail status tells you.

Install in one line. Remove in one line.

$curl -fsSL https://handrail.bitey.ai/install.sh | sh

Prefer to read it first? View the installer — it verifies the release checksum and uses sudo only for the final copy.

Read the docs   View on GitHub