Custom Commands
safe-chains ships definitions for hundreds of tools. To add an in-house CLI or disallow a built-in command, write a TOML file in one of two places:
~/.config/safe-chains.toml: applies everywhere, trusted automatically..safe-chains.tomlin a repo (or any parent directory): applies to that project, once you trust the directory.
When a command is defined in both, a trusted project definition overrides the user-level one.
Project files must be trusted
A repo’s .safe-chains.toml sits inside the code safe-chains checks, and whatever edits the repo can edit that file. safe-chains ignores it until you pin the directory in your user config:
# ~/.config/safe-chains.toml
[[trusted]]
path = "/Users/you/work/acme"
sha256 = "9f2b…" # shasum -a 256 acme/.safe-chains.toml
safe-chains honors the project file only while its contents match sha256. A change to the file un-trusts it until you review the change and trust it again.
Home directory level config (~/.config/safe-chains.toml and ~/.claude/settings.json) is trusted without pinning. Be careful granting access to these directories.
Granting trust requires a manual edit for now.
Add a tool safe-chains doesn’t know
# .safe-chains.toml
[[command]]
name = "myco"
description = "MyCo internal CLI"
url = "https://wiki.myco/cli"
bare_flags = ["--help", "--version", "-h", "-v"]
[[command.sub]]
name = "deploy"
level = "SafeWrite"
standalone = ["--help", "--dry-run", "-h"]
valued = ["--env", "--region"]
max_positional = 1
[[command.sub]]
name = "status"
standalone = ["--help", "--watch", "-h", "-w"]
valued = ["--env"]
[[command.sub]]
name = "logs"
standalone = ["--help", "--follow", "-f"]
valued = ["--service", "--since", "--lines"]
This allows myco --help, myco deploy --dry-run staging, myco status --env prod, and so on. Anything outside the listed flags or subcommands is denied.
The schema mirrors the built-in TOMLs. Every field documented in commands/SAMPLE.toml works in custom files.
Let safe-chains draft it: --suggest
For a command safe-chains doesn’t recognize, --suggest shows you the entry it would take. Pass the command you want to support as the argument:
safe-chains --suggest "acmedeploy --env prod ./service"
It prints three things: a scoped [[command]] block — allowing exactly the flags and positional count you ran, nothing more — the path the .safe-chains.toml belongs at, and the [[trusted]] pin to paste into ~/.config/safe-chains.toml.
It writes nothing. Copy the block into the file yourself, then add the pin. --suggest edits neither file, so nothing takes effect until you have done both by hand. For an entry that applies everywhere, safe-chains config add writes it into ~/.config/safe-chains.toml for you (see Your Rules).
The hash in the pin is for that file with exactly the printed block added. If you change either, recompute it with shasum -a 256 .safe-chains.toml.
The generated level defaults to "SafeWrite"; change it to "SafeRead" or "Inert" if the tool is lighter. --suggest only helps with commands safe-chains doesn’t know — for a recognized command that’s denied by a flag, subcommand, or path, it says so rather than drafting a bypass.
A shell script
# ~/.config/safe-chains.toml
[[command]]
name = "generate-docs.sh"
bare = true
max_positional = 0
Names match the command’s basename. ./generate-docs.sh, bin/generate-docs.sh, and generate-docs.sh all look up the same entry.
Disallow a built-in command for this project
[[command]]
name = "gh"
deny = true
Three lines and gh is denied in this project: bare invocation, subcommands, and every flag.
Generate one with an AI
Paste your tool’s --help output and this prompt into Claude or another LLM:
Generate a safe-chains custom command definition. Use the schema in https://github.com/michaeldhopkins/safe-chains/blob/main/commands/SAMPLE.toml. Output a single TOML block I can paste into
.safe-chains.toml. Cover read-only and idempotent subcommands; omit destructive ones.
If you paste it into a repo’s .safe-chains.toml, pin the directory afterward (see Project files must be trusted).
Skipping custom files: SAFE_CHAINS_NO_LOCAL
Set SAFE_CHAINS_NO_LOCAL=1 to skip the project-local walk and the user-level lookup. Two reasons:
Debugging. If a custom definition might be interfering with a command, run with the bypass to compare against built-in behavior:
SAFE_CHAINS_NO_LOCAL=1 safe-chains "your command"
Slow filesystems. Each invocation makes a few stat() calls walking up from the current directory. On a local SSD this is microseconds. On network mounts (NFS, corporate file shares), WSL1 with files on the Windows side, or under aggressive antivirus, each stat() can cost tens of milliseconds. If you don’t use custom commands and you’re on one of those filesystems, export the variable in your shell init:
export SAFE_CHAINS_NO_LOCAL=1
Now safe-chains skips the lookup entirely.