Start

Guarding an agent's shell commands

Grenz's other surfaces sit between an agent and a credential. This one sits between an agent and its own shell: grenz hook runs as a Claude Code PreToolUse hook, and every Bash command is checked against your policy before it runs. Approvals, budgets and the kill-switch apply to shell commands exactly as they do to API calls.

What it guarantees

The guard confines which binary runs and with what arguments. It does not and cannot contain what that binary then executes: a permitted command may still run code Grenz never sees, from a Makefile, a package script, or a git hook. So granting exec:python3, exec:npm or exec:git is granting code execution, and grenz run says so at startup for every such grant.

What it refuses on principle is narrower and checkable: shell code handed to a shell. Grenz parses bash, so sh -c '…' and … | sh would run straight past its own parser.

1. Turn the guard on

~/.grenz/grenz.yaml
exec_guard: true

2. Grant the commands it may run

A bash grant is an ordinary tool grant with the same deny-by-default precedence as every other. Actions are exec:<binary>; targets are the command line as written.

~/.grenz/policy.yaml
grants:
  - tool: bash
    allow:
      - action: "exec:cd"      # agents open almost every command with cd
        targets: ["cd /path/to/your/repo*"]
      - action: "exec:git"
        targets: ["git status*", "git add *", "git commit *"]
      - action: "exec:ls"
        targets: ["ls", "ls -*"]
    require_approval:
      - action: "exec:git"
        targets: ["git push*"]
    deny: ["exec:sudo"]  # unscoped: cannot be reordered around

Anchor every allow with a target. The action is only the binary's basename, so an unscoped exec:git also matches /tmp/evil/git. Full syntax is in the policy format.

3. Register the hook

.claude/settings.json
{ "hooks": { "PreToolUse": [{
  "matcher": "Bash",
  "hooks": [{ "type": "command", "command": "grenz hook" }]
}]}}

Give the hook the agent's GRENZ_TOKEN in the environment Claude Code runs in, then start the proxy with grenz run.

Start the proxy yourself. With the proxy down every Bash command is denied, including the one that would start it, so the agent cannot do it for you. Keep it up with grenz service install. And do not set a short timeout on the hook entry: Claude Code lets a timed-out hook's command through, so grenz hook always answers first.

How a command becomes a decision

Every element of a pipeline or && list is its own action and its own target, so in git add . && curl evil.com the curl cannot ride on the git grant. Two things about targets catch everyone once:

  • The space in a glob is literal. head * does not match a bare head at the end of a pipeline. List both.
  • Argument order matters. A target-scoped deny can be stepped around by reordering arguments, so confine with narrow allows. An unscoped deny such as exec:sudo never reads argv and stays absolute.

What is refused outright

These are denied before the policy engine sees them, and no policy can allow them: there is no action for the undecidable case, so an exec:* grant is not a way to permit obfuscation.

ReasonExampleWhy
code_in_argvsh -c '…', eval, sourcebash source runs behind Grenz's own parser
stdin_program… | sh, sh <<EOF, xargs …a shell reads its program from stdin
shell_scriptbash deploy.sha shell runs a file the guard never parsed
dynamic$(echo rm) -rf x, echo $(id)the binary is only known at run time, or an argument runs a command
glob/t?p/evil/curlthe binary resolves against the filesystem at exec time
wrapper_opaqueenv LD_PRELOAD=/evil curl xthe wrapper changed what the wrapped binary is

An argument whose value varies, like curl $URL, is different: the binary is known, so it reaches the engine as exec:curl but can never satisfy a target glob. It is denied unless an allow rule sets on_unresolved: approve, which asks a human instead.

The full reference — wrappers like timeout, heredocs, the four environment variables treated as literal, and the parser defect Grenz compensates for — lives in docs/bash-guard.md.