Every bash tool call must provide both command and description as named fields.
bash({
command: "date -u '+%Y-%m-%dT%H:%M:%SZ'",
description: "Get current UTC timestamp"
})
| # | Rule |
|---|---|
| 1 | command is a single string (never an array of tokens). |
| 2 | description is a short non-empty sentence. |
| 3 | Missing either field → tool-call validation error → fix and retry. |
| 4 | Use mode: "sync" by default; raise initial_wait (e.g. 120 s) for builds, MCP warm-ups, and analysis pipelines. |
| 5 | Chain dependent commands with && inside one command string; separate sessions do not share state unless you pass the same shellId. |
| Do | Avoid |
|---|---|
Quote every expansion: "$VAR", "${ARR[@]}" |
Bare $VAR adjacent to other text — splitting / glob surprises |
Use ${VAR:-default} for defaults |
Multi-line if [ -z "$VAR" ]; then VAR=…; fi for a simple fallback |
set -Eeuo pipefail at the top of any multi-step inline script |
Ignoring non-zero exits |
LC_ALL=C.UTF-8 LANG=C.UTF-8 when the step writes Swedish text |
Leaving the default C locale, which may corrupt ö, ä, å |
$(cmd) for command substitution |
Deprecated backticks `cmd` |
Explicit redirection (> /tmp/out 2> /tmp/err) |
Leaving stderr on the runner log unintentionally |
Parameter expansion (${VAR}, ${VAR:-x}, ${VAR##*/}, …) and command substitution ($(cmd)) are safe under the agentic-workflow firewall — the firewall inspects outbound network egress, not shell syntax. Process substitution <(…) is best avoided because some runners disable /dev/fd.
The execution sandbox rejects commands containing any of the following patterns before they run. Rewrite using the safe equivalent instead of trying to work around the block.
| Banned pattern | Why it's blocked | Safe equivalent |
|---|---|---|
${var@P} / ${var@Q} / ${var@E} / ${var@A} / ${var@a} |
These parameter transformations can produce shell-reparsable fragments or syntax-bearing representations, which can smuggle attacker-controlled content into later parsing steps — a known prompt-injection vector. | Expand the target explicitly: printf '%s' "$var", or a plain "$var" substitution. |
${!var} |
Indirect expansion uses $var's value as another variable name — lets an attacker-controlled string pick which variable is read. |
Use an associative array: declare -A MAP; MAP[foo]=bar; echo "${MAP[$key]}". |
Nested $(…$(…)…) |
Builds a command string dynamically from inner results — the classic staged injection shape. | Split into two lines with a temporary variable: inner=$(cmd2); outer=$(cmd1 "$inner"). |
Chained builder assignments that progressively construct a command substitution (a=foo; b="$a"bar; c=$($b)) |
Same staged-injection shape, just spread across multiple statements. | Construct commands as arrays, invoke via "${cmd[@]}"; never re-parse a string as a command. |
eval on variable contents (or eval-like constructs such as bash -c "$var", source /dev/stdin <<<"$var") |
Direct arbitrary-code execution from data. | Never required for our workflows — refuse and rewrite using arrays, case, or explicit branches. |
echo "…text $(cmd) more text…" with other $(…) elsewhere in the same command string |
The gh-aw AWF sandbox (observed across v0.69.3 – v0.74.3) flags any $(…) that lives inside a double-quoted echo/printf string alongside a second unrelated $(…) as "nested command substitution" even when the two are not nested. This is a false positive but the block still fires. |
Split into two lines: RESULT=$(cmd); echo "…text $RESULT more text…". Prefer printf '%s\n' "$RESULT" over echo when the value may contain backslashes. |
Bash arrays built inline and later expanded with "${arr[@]}" in the same command string, e.g. REQ=(README.md foo.md); for f in "${REQ[@]}"; do …; done |
The gh-aw AWF sandbox (observed across v0.69.3 – v0.74.3) has flagged the (…) + [@] combination as a "dangerous expansion" even though the array only contains literal filenames. Treat it as blocked and rewrite. |
Write the file list to a temp file and loop over that: printf '%s\n' README.md foo.md > /tmp/req-$$ && while IFS= read -r f; do …; done < /tmp/req-$$. For small fixed lists, unroll the loop: for f in README.md foo.md; do …; done. |
These rules apply equally to inline bash in prompts AND to bash commands the agent composes at runtime. The sandbox rejects matching commands before they run. If a command is blocked, do not retry the exact pattern — rewrite using the safe equivalent on the first retry. Observed cost: each retry burns ~30–60 s of agent wall time, which can push the run past the safeoutputs MCP idle-session window (07-commit-and-pr.md §Deadline enforcement).
- Never pass secrets through
$(…)into a log-visible command — echoingcurl -H "Authorization: $(…)"will leak if the step is rerun in debug. - Expose secrets through the step's
env:block (for exampleenv: { FOO: <GitHub-Actions secrets expression for FOO> }) rather than inlining a raw secrets expression inside the prompt; the runner masks secret values in output. - Note: this prompt file is loaded via
runtime-import, and the gh-aw validator rejects any GitHub Actions template expression (the double-curly-brace syntax used forsecrets,env,inputs, etc. in workflow YAML) that is not on the safe allow-list — so never embed such an expression in prompt modules, even inside code spans. Keep secret references in the workflow YAML only.
- Use
/tmp/<descriptive-name>-$$(PID suffix) for per-step temp files. - Delete them before the run ends (or rely on the runner wipe).
- Never write temp files under the repo working tree — they will be picked up by
git addand leak into the PR.
- All committed files must be native UTF-8 (
ö,ä,å). Never substitute HTML entities (ö) for Swedish characters. - Set
LC_ALL=C.UTF-8 LANG=C.UTF-8on any bash step that edits markdown or HTML.
- Both
commandanddescriptionare present and non-empty. - Every variable expansion that might contain whitespace or
*is double-quoted. - No backticks, no
<(…)process substitution. - Any file path is absolute or clearly rooted at
$GITHUB_WORKSPACE. - Output redirection (
>,| tee) writes to/tmp/, not the repo root.