Custom Commands
safe-chains ships definitions for hundreds of tools. If your project uses an in-house CLI it doesn’t recognize, or you want to disallow a built-in command for a specific project, drop a TOML file in either of these locations:
.safe-chains.tomlin your repo root (or any parent directory)~/.config/safe-chains.tomlfor definitions that apply across all your projects
When a command appears in more than one custom definition, the more local definition is used.
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.
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.
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.