~/bash-gate $ cat README.md | head -4

Static analysis of Bash commands: every program a command line may run, with its subcommands, options, paths and effects, and a policy that allows, asks about or denies it.

Built for gating the commands an AI agent runs. No Node APIs, no native code: the analysis below is the library itself, running in your browser.

./tutorial --start →

Try it

try:

# what it sees through

Each row is checked live against the recommended policy. Click one to open it above.

# how it works

  1. [1/6] parse

    With unbash, pinned. Any parse error, nested ones included, is a finding, so the command can't be allowed. So is a carriage return in code, which Git Bash on Windows drops and Bash on Linux keeps.

  2. [2/6] resolve words

    Quotes and escapes removed, braces expanded, known variables substituted, unquoted expansions split. A word is static text, or one dynamic argument that says why it's dynamic and whether it may become several words.

  3. [3/6] walk with shell state

    Variables, functions, aliases, hash -p, enable, cd, exports into child shells. Loops are walked until they settle; subshells, pipelines and substitutions get copies of the state.

  4. [4/6] dispatch

    As Bash does: alias, function, builtin, program. eval, trap, bash -c, bash <<EOF, ssh host cmd, git aliases and prompt strings are parsed and walked as code.

  5. [5/6] read argv with a spec

    Options under canonical names (-fr, --forc → --force), subcommands, paths read and written, and the command lines wrappers run: sudo, env, xargs, find -exec…

  6. [6/6] decide

    Every condition matches, doesn't, or may: a dynamic word could match it. Deny beats ask, ask beats allow, and a deny rule that only may match asks.

# three layers

01 analyze

What the command runs, without a policy: every invocation with its shape, wrappers, effects, reads and writes.

analyze('sudo -u root rm -rf "$dir"').invocations
  .map((i) => [i.shape, i.via, i.effects]);
// [['sudo --user=<v>', [], ['privilege']],
//  ['rm --force --recursive <dynamic>', ['sudo'], ['fs:delete']]]

02 check

The analysis under a policy. Hand it a filesystem, and it walks git hooks, git config and npm scripts too.

check('git push origin +main', recommended).decision;
// 'deny': a + refspec forces
check('rg TODO src | head', recommended).decision;
// 'allow'

03 trace

Static analysis can be wrong. On Linux, run the command under a ptrace supervisor that refuses any exec the analysis didn't predict.

const result = await run('ls; cat secret',
  { analysis: analyze('ls'), cwd: '/repo' });
result.stderr;
// 'bash: line 1: /usr/bin/cat: Operation not permitted'

# your own rules

A policy is a list of rules and a default. recommended is a starting point; the tutorial lets you edit one live.

import { allow, ask, deny, type Policy } from 'bash-gate';

const policy: Policy = {
  default: 'ask',
  rules: [
    allow('git status|log|diff'),
    deny('git push', { options: ['-f', '--force-with-lease'], reason: 'No force pushes' }),
    deny('rm', { options: ['-r'], args: ['/', '/*', '~', '~/*'] }),
    ask({ effects: ['network', 'exec:arbitrary'] }),
    ask({ writes: ['..', '../**', '~/**'] }),
  ],
};