Tired of endless prompts from Claude Code?
Do you want to proceed?
❯ 1. Yes
2. Yes, and don't ask again for this risk-free command
3. No
My job has turned into approving Claude in a loop across a bunch of split terminals. Yes. Tab. Yes. Tab. Yes. Oops — no, that one was actually a question it was asking me. You know the drill.
On the other hand, I don't feel lucky enough to drop all permissions and let any command run. I want Claude to stop asking me to approve harmless commands — or commands with harmless side effects (yes, you can create a file in my workspace, that's kind of the point). But when it's about to do something truly stupid ("oh right, if I had run that command it would have wiped all your prod data"), I want the normal Claude prompt back.
Claude Code has a built-in, well-documented mechanism for this: allowlists in the settings. But the granularity is not great. Allowing Bash(kubectl get:*) lacks precision on subcommands and fails to match kubectl -c foobar get pod.
agent-callable grew out of kubectl-readonly, a kubectl wrapper that only allows read-only operations. The kubectl policy engine (kubepolicy) proved useful enough that I generalized the approach to cover every CLI tool an agent might call. agent-callable imports kubepolicy directly for its kubectl filtering — same battle-tested rules, broader scope.
agent-callable is two things:
- A binary that filters shell commands based on TOML config files for simple cases, plus edge cases handled in Go.
- A Claude Code plugin that wraps agent-callable in a transparent PreToolUse hook.
The result: with my agent-callable config, Claude Code no longer prompts me for anything I consider risk-free. Everything else gets the normal prompt. It's 100% transparent.
No security guarantee. This tool filters commands to reduce accidental side effects from LLM agents. It is not a sandbox, does not isolate processes, and a determined or creative agent may find ways around it. Use it as a convenience layer, not as a security boundary.
go install github.com/evaneos/agent-callable/cmd/agent-callable@latestThen generate the default config:
agent-callable --init-config # creates ~/.config/agent-callable/The binary is usable standalone at this point — any LLM agent can call agent-callable <command> to run filtered commands.
Add the marketplace and install the plugin:
/plugin marketplace add Evaneos/agent-callable
/plugin install agent-callable@Evaneos/agent-callable
That's it — every Bash command now goes through the filter, no allowlist to maintain, no CLAUDE.md to write.
The plugin installs a PreToolUse hook. Every time Claude is about to run a Bash command, the hook quietly calls agent-callable under the hood:
- Claude generates a Bash command — it doesn't know the hook exists
- The hook passes the command to
agent-callable --claude - Allowed → auto-approve, no prompt
- Not allowed → the hook steps aside, Claude Code shows the normal prompt
The hook never blocks anything itself. It just fast-tracks the boring stuff.
Set AGENT_CALLABLE_HOOK_DEBUG=1 to log hook decisions to /tmp/agent-callable-hook.log.
Outside Claude Code — or in any context where an LLM runs shell commands — the agent prefixes its commands with agent-callable:
agent-callable kubectl get pods -A # allowed
agent-callable git push # blocked
agent-callable --sh 'git log | head -5' # compound shell expressionThis requires telling the agent to use the prefix (via CLAUDE.md or equivalent — see SAMPLE_CLAUDE.md). The plugin is simpler since it requires no instructions.
Three categories of side effects:
| Category | Verdict | Examples |
|---|---|---|
| Remote effect — modifies an external service | blocked | git push, kubectl apply, gh pr create |
| Persistent config change — durably alters a tool's behavior | blocked | helm repo add, gcloud config set |
| Local cache/artifact write — useful for investigation | allowed | git fetch, docker pull, gh repo clone |
The rule is simple: when in doubt, block. A false positive (unnecessary prompt) is annoying. A false negative (wiped prod database) is not.
Out of the box, agent-callable ships with built-in filters for 12+ CLI tools. Each one has hand-tuned rules in Go.
Built-in tools (click to expand)
- kubectl — read-only commands, blocks
apply/delete/edit/patch, filters out secret content - git — investigation + local writes (clone/fetch/checkout/add/commit/mv/rm), blocks remote writes and force flags
- gh — read-only + clone/checkout, blocks PR create/merge, issue mutations
- docker — inspection +
pull+runwith restrictions (no--privileged, no host network/pid/ipc, RW mounts only underwritable_dirs) - docker-compose — inspection only (
ps/logs/config/images) - flux —
version,get ...,logs - pulumi — info +
preview(auto-injects--non-interactive), blocks--show-secrets - helm — read-only (
list/status/history/get/show/template/lint/search) - kustomize —
build+cfgread-only - gcloud — conservative allowlist (list/describe/get/show/read/logs)
- npm — read-only +
install/ciwith--ignore-scripts+runrestricted to safe scripts (test, lint, build, etc.) - kubectx, kubectl-crossplane, krew — read-only
- xargs, timeout, nice — wrapper tools: validate the inner command recursively against the same policy
Beyond built-ins, default TOML configs add:
- Text processing —
sed,yqwith conditional write checking (-itriggerswritable_dirs) - TypeScript —
tsc,eslint(--fixtriggerswritable_dirs),prettier(--writetriggerswritable_dirs) - Go —
gofmt(-wtriggerswritable_dirs),go(test/build/vet/mod/...) - Python —
ruff(--fixtriggerswritable_dirs),uv(runrestricted to safe commands like pytest/mypy/ruff),ty - And many more (filesystem, network, system info, etc.) — see
agent-callable --list-tools
Everything lives in ~/.config/agent-callable/. Run agent-callable --init-config to generate sensible defaults, or hand-craft your own.
Drop files in ~/.config/agent-callable/tools.d/:
# Read-only tool: all arguments allowed
[grep]
allowed = ["*"]
# Restricted subcommands
[systemctl]
allowed = ["is-active", "is-enabled", "list-units", "status"]
# Write tool: destination checked against writable_dirs
[cp]
allowed = ["*"]
write_target = "last"
flags_with_value = ["-t", "--target-directory"]
# Conditional write: only check writable_dirs when a write flag is present
[sed]
allowed = ["*"]
write_flags = ["-i", "--in-place"]
write_target = "last"
flags_with_value = ["-e", "-f", "--expression", "--file"]write_target controls which arguments are checked against writable_dirs:
"last"— last positional arg is the destination (cp,mv,ln,sed -i)"all"— all positional args are destinations (mkdir,touch,tee,eslint --fix)
write_flags makes write_target conditional: the check is only enforced when one of the listed flags is present. Without the flag, the command runs freely (read-only mode). Short flags match by prefix (-i matches -i.bak), long flags match exactly or with = (--fix matches --fix=true).
Built-in tools always take priority over config files.
~/.config/agent-callable/config.toml:
writable_dirs = ["/tmp"] # enforced on: redirects, docker volumes, write_target tools
[audit]
file = "~/.local/share/agent-callable/audit.log" # parent dir auto-created
mode = "none" # "none", "blocked", "allowed", "all"
max_entries = 10000 # oldest trimmed on open (0 = unlimited)
mask_secrets = true # mask tokens, passwords, env vars in logged commandsClaude Code rarely runs simple commands. It chains pipes, loops, and conditionals. agent-callable parses the full shell AST — if a compound expression contains only control flow (for, if, &&, ||, pipes) and allowed commands, the entire expression is auto-approved without prompting.
agent-callable --sh 'kubectl get pods | grep Running'
agent-callable --sh 'for ns in prod staging; do kubectl get pods -n $ns; done'
agent-callable --sh 'git status && git diff --stat'This mode is deliberately weaker than single-command mode on argument checking: variables like $ns can't be resolved statically, so only command names are validated. Dynamic commands ($CMD args) and builtins that could bypass validation (eval, exec, source) are blocked. Write redirections are limited to /dev/null and writable_dirs.
agent-callable --audit <tool> [args...] # dry-run: check without executing
agent-callable --audit --sh '<expression>' # dry-run: check a shell expression
agent-callable --claude '<expression>' # JSON output for the Claude Code hook
agent-callable --list-tools # list all registered tools
agent-callable --help-config # config format documentation- Heredocs piped to compound commands (
cat <<'EOF' | while...done) are not parsed by the shell validator — you get the normal prompt. This is a limitation of the Go parsermvdan.cc/sh. cp -t DIR srcstyle invocations (destination as a flag value) are not fully covered bywrite_target = "last".- No argument validation in
--shmode — variables can't be resolved statically, so only command names are checked.