---
title: Bash
icon: Terminal
description: Run shell scripts from an Earl template.
---
The Bash protocol runs a shell script and returns its output. Earl blocks all private and loopback IP ranges at the SSRF layer regardless of sandbox settings — `network = true` only enables outbound connections to public addresses.
## A complete example
```hcl
version = 1
provider = "local"
categories = ["files"]
command "count_lines" {
title = "Count lines"
summary = "Count lines in a file matching a pattern"
description = "Runs grep to count lines matching the given pattern in a file. Returns the count."
annotations {
mode = "read"
}
param "path" {
type = "string"
required = true
description = "Path to the file"
}
param "pattern" {
type = "string"
required = true
description = "Pattern to grep for"
}
operation {
protocol = "bash"
bash {
script = <<-SH
grep -c "$EARL_PATTERN" "$EARL_PATH" || echo 0
SH
env = {
EARL_PATH = "{{ args.path }}"
EARL_PATTERN = "{{ args.pattern }}"
}
sandbox {
network = false
max_time_ms = 5000
max_output_bytes = 65536
}
}
}
result {
decode = "text"
output = "{{ result | trim }} matching lines"
}
}
```
Run it:
```bash
earl call local.count_lines --path /var/log/syslog --pattern ERROR
```
## Walk-through
### bash block
```hcl
bash {
script = <<-SH
grep -c "$EARL_PATTERN" "$EARL_PATH" || echo 0
SH
env = {
EARL_PATH = "{{ args.path }}"
EARL_PATTERN = "{{ args.pattern }}"
}
...
}
```
`script` is the shell script. The heredoc syntax (`<<-SH ... SH`) keeps multi-line scripts readable without escaping. Args are passed through `env` and referenced in the script as `"$VAR_NAME"`. When a variable is expanded with double quotes, the shell treats its value as a literal string — shell operators, command substitution, and word splitting in the value are not interpreted. Rendering args directly into the script string (`{{ args.path }}`) instead of through env vars allows values containing `;`, `$()`, or backticks to inject additional shell commands.
### sandbox block
```hcl
sandbox {
network = false
max_time_ms = 5000
max_output_bytes = 65536
}
```
`network = false` blocks outbound network calls from the script. `max_time_ms` kills the script if it runs past the limit. `max_output_bytes` caps the combined size of stdout and stderr.
Two more fields are available: `max_memory_bytes` limits RSS, and `max_cpu_time_ms` limits total CPU time rather than wall time.
Omitting the `sandbox` block entirely means no limits apply. Always set at least `max_time_ms`.
Note that even with `network = true`, Earl blocks all private and loopback IP ranges (127.0.0.0/8, 10.0.0.0/8, and similar) at the SSRF layer. There is no config option to bypass this.
### result
```hcl
result {
decode = "text"
output = "{{ result | trim }} matching lines"
}
```
Bash output is plain text. `decode = "text"` gives the full stdout string as `result`. Use the `trim` filter to strip trailing newlines before formatting.
## Env vars and writable_paths
Pass string args through `env` and reference them as `"$VAR_NAME"` in the script. This prevents shell injection: a value like `. ; rm -rf .` is passed as a literal string to the command rather than being interpreted by the shell. Rendering args directly into the script (`{{ args.path }}`) does not have this property — values containing `$()`, backticks, or `;` execute as shell code.
```hcl
command "extract_json_field" {
title = "Extract JSON field"
summary = "Extract a field from a JSON file using jq"
description = "Runs jq on a JSON file and writes the result to an output file."
annotations {
mode = "write"
}
param "input_path" {
type = "string"
required = true
description = "Path to the input JSON file"
}
param "filter" {
type = "string"
required = true
description = "jq filter expression (e.g. '.users[].name')"
}
param "output_path" {
type = "string"
required = true
description = "Path to write the output"
}
operation {
protocol = "bash"
bash {
script = <<-SH
jq -r "$JQ_FILTER" "$INPUT" > "$OUTPUT"
echo "Written to $OUTPUT"
SH
env = {
INPUT = "{{ args.input_path }}"
OUTPUT = "{{ args.output_path }}"
JQ_FILTER = "{{ args.filter }}"
}
sandbox {
network = false
max_time_ms = 10000
writable_paths = ["{{ args.output_path }}"]
}
}
}
result {
decode = "text"
output = "{{ result | trim }}"
}
}
```
`writable_paths` grants write access to specific filesystem paths. The script can write there but nowhere else. Paths support Jinja expressions, so you can derive them from params at render time.
---
To switch between environments in scripts, see [Environments](/docs/environments).
For naming conventions and other patterns that apply across all protocols, see [Best Practices](/docs/best-practices).
For streaming stdout line-by-line, see [Streaming — Bash streaming](/docs/streaming#bash-streaming). For the full field reference, see [Template Schema — Bash](/docs/template-schema#bash).