Policy engine for controlling command execution and filesystem access.
A %CommandPolicy{} struct is stored as a top-level field on Bash.Session state.
It is immutable once the session is initialized — set, shopt, and other runtime
mechanisms cannot modify it.
Fields
:commands— gates command execution across all categories: builtins, externals, shell functions, and Elixir interop calls.:paths— gates all filesystem access. Every resolved absolute path is checked against these rules before any filesystem operation (cd, source, redirects, test operators, glob expansion).
Command Categories
Every command resolved by the interpreter falls into one of four categories:
:builtin— built-in shell commands (echo,cd,export, etc.):external— external OS commands (cat,grep,/usr/bin/sort, etc.):function— user-defined shell functions:interop— Elixir interop commands registered viaBash.Interop
Command Policy Types
:unrestricted— all commands in all categories are allowed (default).:no_external— block all external command execution; other categories allowed.- A list of rules — evaluated in order, first match wins.
- A function
(String.t(), command_category() -> boolean())— dynamic evaluation per command and category. - A function
(String.t() -> boolean())— legacy dynamic evaluation, only applied to external commands.
Rule Evaluation
When commands is a list, each rule is tried in order:
{:allow, items}— if the command matches any item, it is allowed.{:disallow, items}— if the command matches any item, it is denied.{:allow, :all}— allows all commands unconditionally.{:disallow, :all}— denies all commands unconditionally.fun/2— receives(name, category), if it returnstrue, the command is allowed;falsecontinues to the next rule.fun/1— legacy; only evaluated for:externalcommands, skipped for others.
Items can be strings (exact or basename match), Regex patterns, or category atoms:
:builtins— matches all commands in the:builtincategory:externals— matches all commands in the:externalcategory:functions— matches all commands in the:functioncategory:interop— matches all commands in the:interopcategory
If no rule matches, the command is denied.
Common Recipes
The table below shows how to set up policies for common scenarios. Categories
not listed in an {:allow, [...]} rule are denied by default (fail-closed).
| Goal | Policy |
|---|---|
| Allow everything (default) | commands: :unrestricted |
| Block all externals | commands: :no_external |
| Builtins only | commands: [{:allow, [:builtins]}] |
| Externals only | commands: [{:allow, [:externals]}] |
| Builtins + functions | commands: [{:allow, [:builtins, :functions]}] |
| Builtins + externals | commands: [{:allow, [:builtins, :externals]}] |
| Builtins + interop | commands: [{:allow, [:builtins, :interop]}] |
| Builtins + functions + interop | commands: [{:allow, [:builtins, :functions, :interop]}] |
| Everything except externals | commands: [{:disallow, [:externals]}, {:allow, :all}] |
| Everything except interop | commands: [{:disallow, [:interop]}, {:allow, :all}] |
| Block specific builtins | commands: [{:disallow, ["eval", "source"]}, {:allow, :all}] |
| Builtins + specific externals | commands: [{:allow, [:builtins, "cat", "grep"]}] |
| Block everything | commands: [{:disallow, :all}] |
Examples
# Block all external commands (shorthand)
%CommandPolicy{commands: :no_external}
# Allow only builtins and specific externals
%CommandPolicy{commands: [{:allow, [:builtins, "cat", "grep"]}]}
# Allow builtins and functions, block everything else
%CommandPolicy{commands: [{:allow, [:builtins, :functions]}]}
# Block eval and source, allow everything else
%CommandPolicy{commands: [{:disallow, ["eval", "source"]}, {:allow, :all}]}
# Regex-based rules
%CommandPolicy{commands: [{:allow, [~r/^git-/]}]}
# Category-aware function
%CommandPolicy{commands: fn _cmd, cat -> cat in [:builtin, :function] end}
Summary
Functions
Returns true if the policy allows any external commands at all.
Checks whether the given command is allowed under the policy.
Checks whether access to the given filesystem path is allowed under the policy.
Returns true if the specific command is allowed under the policy.
Extracts the command policy from session state.
Builds a %CommandPolicy{} from a keyword list, map, or existing struct.
Returns true if the given path is allowed under the policy.
Types
@type command_category() :: :builtin | :external | :function | :interop
@type t() :: %Bash.CommandPolicy{commands: command_rule(), paths: path_rule()}
Functions
Returns true if the policy allows any external commands at all.
Used by pipeline optimization to decide if the streaming path is viable.
@spec check_command(t(), String.t(), command_category()) :: :ok | {:error, String.t()}
Checks whether the given command is allowed under the policy.
The category argument identifies what kind of command is being checked:
:builtin, :external, :function, or :interop. Defaults to :external
for backwards compatibility.
Returns :ok or {:error, message} with a descriptive error string.
Checks whether access to the given filesystem path is allowed under the policy.
Returns :ok when paths is nil (no restrictions) or the path passes the rules.
Returns {:error, message} when the path is denied.
@spec command_allowed?(t(), String.t(), command_category()) :: boolean()
Returns true if the specific command is allowed under the policy.
Extracts the command policy from session state.
Returns the default (unrestricted) policy when none is configured.
Builds a %CommandPolicy{} from a keyword list, map, or existing struct.
String lists inside {:allow, list} and {:disallow, list} are partitioned
into a MapSet (for O(1) string lookup), a list of non-string matchers
(regex, functions), and a MapSet of category atoms.
iex> CommandPolicy.new(commands: :no_external)
%CommandPolicy{commands: :no_external}
iex> CommandPolicy.new(commands: [{:allow, [:builtins, "cat", ~r/^git-/]}])
# categories -> MapSet, strings -> MapSet, regex kept separately
Returns true if the given path is allowed under the policy.