Handrail documentation
Handrail installs policy packs for coding agents where the agent cannot change them. This version supports Claude Code on macOS and Linux.
Install
One command:
curl -fsSL https://handrail.bitey.ai/install.sh | sh
The installer detects your OS and architecture, downloads the matching binary from the GitHub release, checks its SHA-256 against the release's SHA256SUMS, and installs it to /usr/local/bin. Only that final copy uses sudo. It does not change any agent configuration.
| Variable | Effect |
|---|---|
HANDRAIL_VERSION=v0.1.0 | Install a specific release instead of the latest. |
HANDRAIL_INSTALL_DIR=… | Install somewhere else. See the note below. |
HANDRAIL_VERIFY_PROVENANCE=1 | Also require gh attestation verify to confirm the binary was built by this repository's release workflow. |
Keep the binary where only root can write. Handrail runs itself through sudo to change enforced policy. A binary your user can overwrite could be replaced by anything running as you before sudo runs it. handrail doctor checks this.
Read the installer first
curl -fsSLO https://handrail.bitey.ai/install.sh less install.sh sh install.sh
Verify provenance
Release archives carry a signed build-provenance attestation. With the GitHub CLI:
gh attestation verify handrail-aarch64-apple-darwin.tar.gz --repo martinx/handrail
Other ways to install
Instead of the script, you can use one of:
| With | Command |
|---|---|
| Homebrew | brew install martinx/tap/handrail |
| Cargo | cargo install handrail |
Both install into a directory your user can write (the Homebrew prefix, ~/.cargo/bin). For enforced policy, the install script above is the safer choice.
Update
handrail self-update --check # is there a newer release? handrail self-update # update
Script installs are updated with the same checksum-verified installer, pinned to the version just reported. Homebrew and cargo installs are pointed at brew upgrade handrail and cargo install handrail: package managers own their files.
Be told about new releases
handrail self-update --auto on # off by default
With the daily check on, handrail statusline shows ↑0.1.2 when a newer release exists. It never installs anything — updating stays one command you run. The status line never waits on the network: it reads a cached answer, and refreshes it in the background at most once a day with a single request to api.github.com. Turn it off with --auto off. With it off, Handrail uses the network only when you run self-update.
Why no automatic install: Handrail runs with administrator rights, so an automatic update would turn a compromised release channel into code running as root on every machine. Homebrew also requires software not to update itself.
From source
git clone https://github.com/martinx/handrail && cd handrail cargo build --release sudo install -m 755 target/release/handrail /usr/local/bin/handrail
Quick start
handrail list # packs, and how strongly Claude Code enforces each handrail show privacy # what a pack protects, its tradeoffs, its limits handrail use baseline --dry-run # the exact files that would change handrail use baseline # apply: confirm, then your password once
Start a new claude session afterwards; running sessions keep their old configuration.
Commands
| Command | What it does |
|---|---|
list [--category c] | Packs by category with tier and enforcement level. * marks installed packs. |
show <pack> | Everything about a pack: what it protects, tradeoffs, limits, per-agent enforcement, profiles it belongs to. |
profiles | Named sets of packs. |
use <profile> | Make the installed packs exactly this profile. Your own rules are kept. |
enable <pack…> | Add packs to what is installed. |
disable <pack…> | Remove packs. --all removes every pack and rule and leaves nothing behind. |
rule add|list|edit|remove | Your own rules, added to the enforced instructions block. Each has an id (rule-7k3m, or your own with --id) to edit or remove it by. |
rollback | Undo the last change. |
status | What is installed, and anything that would make it ineffective. |
statusline | One line for Claude Code's status line: the matching profile or pack count, and drift when installed files differ from what Handrail wrote. Local reads only. |
self-update [--check] [--auto on|off] | Update Handrail itself to the latest release; --auto on enables a daily check shown in the status line (nothing is installed automatically). |
check [dir] | Validate a catalog directory: pack format, hook test vectors, script lint. For pack authors and CI. |
publish <pack> --catalog <dir> | Propose your pack to the shared catalog as a pull request. --repo owner/name targets another catalog repository. See Share and publish. |
doctor | Checks: Claude Code present, binary not user-writable, no overriding policy sources, no interrupted runs, no tampering. |
Every command also accepts --catalog <dir|repo> (repeatable), which adds the packs in a directory or git repository to the built-in ones. See Share and publish.
Every command that changes something accepts --dry-run (show the plan, write nothing) and --yes (skip the confirmation prompt; the password prompt remains).
Two tiers
The agent runs as you. Anything you can change without administrator rights, it can change too — including turning a rule off. So packs come in two tiers:
| Enforced | Advisory | |
|---|---|---|
| For | Security and privacy guarantees | Preferences and conventions |
| Location | Claude Code's managed policy directory | ~/.claude/rules/ |
| Owner | root | you |
| Changing it | Needs your password | No password |
| Agent can undo it | No | Yes, by design |
Separately, each pack states how strongly each agent enforces it:
| Level | Meaning |
|---|---|
| enforced | Applied by the agent's own settings, permission rules, hooks or sandbox, whatever the model does. |
| partial | Enforced for the common paths; the pack's limits say what gets through (for example, a script that opens a file itself). |
| advisory | Instructions only. The model is asked, not forced. |
Where files go
The managed policy directory is /Library/Application Support/ClaudeCode/ on macOS and /etc/claude-code/ on Linux. Settings there take precedence over user, project and command-line settings, and the CLAUDE.md there is loaded in every session and cannot be excluded.
| Path | Contents |
|---|---|
managed-settings.d/handrail-<pack>.json | One fragment per pack. Claude Code merges fragments in name order: lists are combined, objects merge by key. Handrail's test suite rejects two packs that set the same scalar key differently, because the later one would silently win. |
handrail/packs/<pack>/hooks/ | Hook scripts referenced by that pack's settings. |
CLAUDE.md | A block between <!-- handrail:begin --> and <!-- handrail:end -->. Everything else in the file is left alone, and removing the block restores the original bytes. |
handrail/state.json | What is installed, and exactly which files Handrail owns. |
handrail/backups/ | The five most recent backups, used by rollback. |
Handrail never modifies files it did not create, including managed-settings.json.
How changes are made
- Plan. The change is computed as an exact list of file writes and deletions, each with the file's current hash as a precondition.
--dry-runprints it. - Elevate the intent, not the files. For the enforced tier, only which packs and rules you want is passed to
sudo handrail __apply. The privileged process recomputes the plan from the catalog compiled into the binary and refuses unless its hash matches the plan you reviewed. - Apply transactionally. Check every precondition, validate every JSON file (an invalid managed settings file stops Claude Code from starting), stage new content, back up what will be replaced, write a journal, then commit. If the process dies during the commit, the next run restores the previous state from the journal.
Because sudo needs a terminal to ask for your password, an agent running commands without one cannot raise its own privileges to change enforced policy.
Verify it's working
- Source: in a new
claudesession, run/status— the managed policy appears under "Setting sources".claude doctorshows it too, including any entries it dropped. - Instructions: run
/memory— the managedCLAUDE.mdis listed as loaded. - Behaviour: with
privacy, asking for an Artifact is refused with a Handrail message; withanti-bypass,claude --dangerously-skip-permissionsis refused; withaudit, tool calls appear in~/.claude/handrail/audit/. - Integrity:
handrail doctorre-plans what is installed. Any difference means files were edited, deleted or replaced.
In Claude Code's status line
Claude Code runs a command of your choice to draw its status line (statusLine in ~/.claude/settings.json). Add Handrail's segment to it:
"statusLine": { "type": "command", "command": "handrail statusline" }
If you already have a status line script, append $(handrail statusline) to its output. It prints e.g. handrail: baseline ✓, or drift ⚠ when installed files were changed.
Limits
- Model inference sends your conversation to the model API. No local setting changes that. Handrail turns off every optional upload and cloud execution path.
- A local administrator can edit the managed directory. Protecting against the administrator requires MDM.
- If an MDM-delivered or server-managed Claude Code policy exists, Claude Code uses it and ignores file-based policy by default.
handrail statuswarns about this. - Commands you type yourself after Claude Code's
!prefix are yours: they are not sandboxed or blocked.
Your own rules
handrail rule add "Reply in English" # gets an id such as rule-7k3m handrail rule add "Small, focused commits" --id commits handrail rule list # id and text handrail rule edit commits "One logical change per commit" handrail rule remove rule-7k3m commits
Generated ids are rule- plus four characters from an alphabet without look-alikes (no 0 1 i l o). They are random, not derived from the text, so editing a rule keeps its id. Your own ids use lowercase letters, digits and hyphens, up to 32 characters.
Rules land in the root-owned instructions block, loaded in every session. They are instructions to the model, not enforcement.
Writing a pack
Write a pack in any directory; a catalog is just a folder with packs/ in it (and optionally profiles/). The complete example on the home page is a working pack you can copy. Each pack is a directory:
my-packs/packs/my-pack/ ├─ pack.toml # metadata, tier, per-agent spec ├─ rules.md # instructions for the agent (English) ├─ claude-code/ │ ├─ settings.json # merged as a managed-settings.d fragment │ └─ hooks/guard.sh # optional └─ tests/guard.cases # hook test vectors
id = "my-pack" version = "1.0.0" category = "security" tier = "enforced" # or "advisory": rules.md only title = "Short title" summary = "One or two sentences." protects = ["What it guards against"] tradeoffs = ["What the user gives up"] limits = "What it cannot do. Required." [targets.claude-code] enforcement = "enforced" # enforced | partial | advisory | unsupported min_version = "2.1.242" # optional: oldest Claude Code with the keys you use settings = "claude-code/settings.json" hooks = ["claude-code/hooks/guard.sh"]
In settings.json, @PACK_DIR@ is replaced with the directory the pack is installed to, so a hook is referenced as "\"@PACK_DIR@/hooks/guard.sh\"". Test vectors are tab-separated lines: name, expected exit code, JSON input.
handrail check . validates every pack, locally and in that repository's CI: required fields, unknown fields and placeholders, settings that are valid JSON objects, scalar keys that clash with another pack, and each hook's test vectors. Only claim enforced for what the agent actually enforces, and keep limits honest — a pack that claims more than it enforces is a bug.
Share and publish
Try a pack from your directory with --catalog; nothing is written until you confirm the plan:
handrail check ./my-packs handrail enable my-pack --catalog ./my-packs --dry-run handrail enable my-pack --catalog ./my-packs
Handrail installs a copy of an external pack next to the files it writes (in the root-owned directory for enforced packs), so status, disable and later changes keep working without the source, and the privileged step reads the root-owned copy. To install a new version, run enable with --catalog again. list and show say where an external pack came from. An external pack cannot reuse a built-in pack's id or set a setting a built-in pack already sets to a different value.
Share with a link
Push the directory to any git repository; others install from it directly, with no review step:
handrail enable my-pack --catalog github.com/you/my-packs handrail enable my-pack --catalog git@github.com:your-org/policy-packs.git
Only install packs from people you trust: an enforced pack's hooks run on every matching tool call.
Publish to the shared catalog
handrail publish my-pack --catalog ./my-packs --dry-run handrail publish my-pack --catalog ./my-packs
publish runs handrail check, then opens a pull request on martinx/handrail-packs through the GitHub API: it forks the repository into your account when you can't push to it, commits the pack on a branch pack/<id>-<version>, and opens the pull request. Publishing the same version again updates that pull request; files removed from the pack are removed in it too.
- Access: the GitHub CLI when signed in (
gh auth login), otherwise a token inGITHUB_TOKENorGH_TOKENwith thepublic_reposcope. The token is passed to curl on stdin, never on the command line. With neither, the manual steps are printed. - Review: every pull request is reviewed by a maintainer (see the contributing guide). Merged packs ship in the next Handrail release.
- Team catalogs:
--repo your-org/policy-packssends the pull request to your own repository instead.
Roadmap
| Next | What |
|---|---|
| Registry | Signed, versioned packs fetched with handrail update: shows a diff, never auto-applies executable content. |
| Terminal UI | Browse and toggle packs by category, with the enforcement matrix. |
| More categories | Communication, git hygiene, code quality, testing, cost. |
| More agents | Chosen by demand, each with a published enforcement matrix. |
Uninstall
handrail disable --all # remove every pack and rule sudo rm /usr/local/bin/handrail # remove the binary
A backup of the removed files is kept in your temporary directory until reboot.
FAQ
Is this made by Anthropic?
No. Handrail is an unofficial, independent project. It only uses configuration that Claude Code documents.
Does Handrail send anything anywhere?
No. It has no telemetry and no account. The CLI works offline and only goes online when you ask it to: self-update, --catalog with a repository, and publish. The opt-in daily update check reads the latest release number from GitHub.
Where do pack updates come from?
The built-in packs are compiled into the binary, so an install always has the packs of that release. Packs are maintained in handrail-packs; changes there are synced into Handrail daily and shipped with the next release. Get them with handrail self-update (or brew upgrade handrail), then run handrail status: installed packs older than the binary's are reported, and re-running handrail use <profile> or enable updates them.
Why does it need my password?
Because the point is that the agent — running as you — cannot change the policy. The same property means changing it needs administrator rights.
What if something goes wrong?
handrail rollback undoes the last change; handrail disable --all removes everything. Interrupted runs are rolled back automatically.