A configurable checkpoint for sensitive operations in your dev environment. Stop. Confirm. Proceed.
tollgate is a small CLI tool that intercepts sensitive operations against external services and asks you to confirm them. It works by placing shim binaries named git and gh on your $PATH ahead of the real ones. When a watched command runs (git push, gh pr create, gh repo delete), the shim pauses, prompts you in the terminal, and only proceeds if you say yes.
It exists because agents now write and run code in your environment, and the operations that touch the outside world deserve a moment of human attention. tollgate is that moment.
$ git push origin main
tollgate: allow git [push origin main]? [y/n/a] y
[ ... real git push proceeds ... ]
Most agent-driven workflows lack human-in-the-loop checkpoints for destructive or external operations. Pushing to a remote, creating a pull request, deleting a repository, deploying to production: these are operations where a one-token mistake by an agent has real consequences.
tollgate gives you a configurable set of "ask first" gates. It's not a sandbox; it's a tap on the shoulder. The friction is the feature.
The design doesn't try to distinguish "agent" from "user." Both go through the same prompt. If you don't want a prompt for your own work, you have an environment variable kill switch. The model stays honest.
When installed, tollgate puts two shim binaries (git and gh) into ~/.tollgate/bin/. You add that directory to the front of your $PATH. From then on, any process that runs git or gh hits the shim first.
The shim's decision sequence:
- Check the
TOLLGATEenvironment variable. If set tooff, exec the real binary immediately and exit. - Match the invocation against a configurable watchlist (
git push,gh pr create, etc.). If unwatched, exec the real binary and exit. - Consult current state: global on/off, session pause, prior session-allow rules.
- If a prompt is needed, write a question to
/dev/ttyand read the response. - On allow, exec the real binary. On deny, exit non-zero.
- Log the decision to
~/.tollgate/audit.log.
Unwatched invocations (git status, gh pr list, etc.) pass through with negligible overhead. The shim uses syscall.Exec to replace itself with the real binary rather than forking and waiting on it.
The shim approach relies on $PATH inheritance. When any process runs git or gh, the OS searches $PATH in order and runs the first match. The shim wins by being first.
Interception depends on two things being true at the same time:
~/.tollgate/binis in$PATH- The process running the command inherited that
$PATH
The common case that works: You open a terminal, your shell sources .zshrc, and you launch Claude Code from that terminal. Every bash command Claude runs inherits that $PATH. Subagents, background tasks, parallel tool calls: all protected, because they are all subprocesses of the same Claude Code instance.
What won't be protected by default:
- A Claude Code session that was open before you ran
tollgate installand updated.zshrc. That session inherited the old$PATH. Open a new terminal to pick it up. - Cron jobs, launchd services, and CI runners. These typically don't source
.zshrcand start with a minimal environment. They need explicitPATHsetup in their own config. - Any process that calls
gitorghusing a full absolute path (e.g./opt/homebrew/bin/git) instead of relying on$PATHlookup.
Quick check: Run which git after install. If it returns ~/.tollgate/bin/git, the current session is protected. If it returns anything else, run source ~/.zshrc or open a new terminal.
git clone https://github.com/rockwellwindsor/tollgate
cd tollgate
make build
./bin/tollgate installtollgate install writes the shim binaries into ~/.tollgate/bin/ and prints the one-line addition for your shell profile:
export PATH="$HOME/.tollgate/bin:$PATH"After updating your shell config, reload it (source ~/.zshrc) or open a new terminal.
Verify the install:
which git
# /Users/you/.tollgate/bin/git <- good, this is the shim
tollgate status
# tollgate: ENABLED
# audit: 0 entriestollgate status show current state, active rules, and audit log count
tollgate pause disable prompts for this shell session
tollgate resume re-enable prompts for this shell session
tollgate on enable globally
tollgate off disable globally (across all sessions)
tollgate clear-logs delete the audit log (with confirmation)
tollgate install write/update shim binaries and config
When a watched command is intercepted:
tollgate: allow git [push origin main]? [y/n/a]
| Key | Meaning |
|---|---|
y |
Allow this once |
n |
Deny this once |
a |
Allow this pattern for the rest of this shell session |
Bypass tollgate for a single invocation:
TOLLGATE=off git pushOr for a whole session:
tollgate pause
# ...do stuff freely...
tollgate resumeOr persistently across all sessions:
tollgate off # stays off until you run `tollgate on`The three layers (env var, session pause, global toggle) are independent. Use whichever fits the situation.
The config file lives at ~/.tollgate/config.json (override the directory with TOLLGATE_HOME).
Out of the box, tollgate watches:
git push(any push to a remote)git push --force/-f(separate pattern from plain push)gh pr creategh pr mergegh repo creategh repo deletegh release create
Everything else passes through silently.
The default_action field controls what happens when no session rule applies:
| Value | Behavior |
|---|---|
prompt |
Ask every time (default) |
allow |
Log but never prompt, "soft off" |
deny |
Block all watched ops without prompting |
{
"default_action": "prompt"
}Three layers of disable, in increasing scope:
| Method | Scope | When to use |
|---|---|---|
TOLLGATE=off cmd |
One invocation | One-off bypass during manual work |
tollgate pause / tollgate resume |
One shell session | A chunk of manual work in one terminal |
tollgate on / tollgate off |
All sessions, persistent | Stepping away from agent work entirely |
No time-bounded resume. Time-bounded toggles behave like a cache, and caches in dev tools tend to mislead you about current state. If tollgate is on, it's on. If you turned it off, it's off until you turn it back on.
Every watched invocation writes a line to ~/.tollgate/audit.log as JSONL:
{"binary":"git","args":["push","origin","main"],"pattern":"git-push","decision":"allowed-once"}Pass-through (unwatched) invocations are not logged. Only watched ones.
To clear the log:
tollgate clear-logs # interactive confirmation
tollgate clear-logs --dry-run # show what would be deleted, delete nothing
tollgate clear-logs --yes # skip confirmationclear-logs only ever touches the audit log. It will not remove your config, your shims, or any session state.
- Errors as values is verbose, but honest. Every place a thing can fail is visible in the code. After a while, I stopped wishing for
try/catch. - Table-driven tests make TDD feel natural. A fast compile loop and a standard library
testingpackage with no setup ceremony made test-first feel like the path of least resistance, not a discipline. syscall.Execis a magic word. The unwatched-path performance trick (replacing the shim process with the real binary rather than forking and waiting) is the kind of thing you can only do when the language exposes the OS directly. Worth knowing about.- Single-binary distribution is the underrated feature. No runtime, no
node_modules, no virtualenv. The install story ismake buildand that's it.
A longer writeup of the design decisions and what changed about my thinking is at windsordevelopmentstudio.io/post/tollgate-confirmation-layer-for-ai-agents.
- Per-directory enable/disable (a
.tollgate-offfile walks up from cwd) - Optional desktop notifications alongside terminal prompts
- Windows support (PATHEXT handling, signal semantics, cmd.exe quirks)
- Configurable log rotation policy
- Vercel support with patterns for
vercel deploy --prod,vercel env,vercel domains - Shims for
npm publish,cargo publish, and similar package-publish operations
Open issues track all of the above. Contributions and design suggestions welcome.
Built with Go 1.22+. Tests live alongside the code they test. End-to-end tests live in tests/e2e/ and spawn real shim binaries against a synthetic $PATH.
make test # run all tests
make lint # run golangci-lint
make build # build all binaries to ./bin/
make test-coverage # generate HTML coverage reportMIT. See LICENSE.
Built by Rockwell Windsor Rice