Skip to content

feat(config): single-command bash whitelist + env-var leak guard#618

Open
edenreich wants to merge 15 commits into
mainfrom
feat/bash-whitelist-append-env-vars
Open

feat(config): single-command bash whitelist + env-var leak guard#618
edenreich wants to merge 15 commits into
mainfrom
feat/bash-whitelist-append-env-vars

Conversation

@edenreich
Copy link
Copy Markdown
Contributor

@edenreich edenreich commented Jun 6, 2026

Summary

Hardens what the Bash tool auto-approves (the whitelist consulted by the Bash tool, the approval policy, and agent auto-approval) and completes the unified commands model.

Auto-approval policy (config/bash_whitelist.go)

  • Single-command only: any top-level operator (|, &&, ||, ;, &, newline) drops the whole command to approval; operators inside quotes (jq '… | …', --title "a && b") still count as one command. Closes the echo x | xargs rm prefix hole.
  • Env-var leak guard: $VAR may be used in commands, but expanding one in a command that prints (echo/printf) or publishes (gh issue/pr create|comment|edit) is rejected, so the agent can't echo or post a secret's value. A single-quoted or backslash-escaped $ stays literal.
  • git push is never in the default list, so it always requires approval.
  • Kept: command substitution, file-write redirects, and dangerous find actions are rejected. Each rejection now returns an actionable hint to the model.

Unified list + flags (earlier commits in this PR)

  • Single commands regex list (replaces the old commands+patterns split), INFER_TOOLS_BASH_WHITELIST_COMMANDS[_APPEND], and matching persistent flags.

Behavior change

Commands that previously auto-approved (compound/piped commands, echo $VAR) now require approval. CI callers can re-add specific safe commands via INFER_TOOLS_BASH_WHITELIST_COMMANDS_APPEND (follow-up: infer-action will append git commit/git push).

Verification

go test ./..., golangci-lint, gofmt, and markdownlint all pass. Also fixed pre-existing stale test configs left by the unified-model commit.

Add INFER_TOOLS_BASH_WHITELIST_COMMANDS_APPEND and
INFER_TOOLS_BASH_WHITELIST_PATTERNS_APPEND. Unlike the existing
INFER_TOOLS_BASH_WHITELIST_COMMANDS / _PATTERNS vars (which replace the
resolved list), the _APPEND siblings merge onto the fully-resolved
default/config/replace list, so callers can add per-repo entries
without clobbering the built-in default.

Applied after ReadInConfig so the merge sees the final base. The CLI
default stays the single source of truth for the whitelist; consumers
(infer-action, the org reusable workflow) are pass-throughs that only
forward these vars.

Also extract the shared comma/newline list parsing into
parseDelimitedList, removing the duplicated loop across the five INFER_*
list env vars and keeping initConfig under the gocyclo ceiling.
@edenreich edenreich requested a review from a team as a code owner June 6, 2026 22:21
… dir

config.DefaultLogsPath is relative (".infer/logs"), so every test that
calls initConfig() (which calls logger.Init) created a real
cmd/.infer/logs/ directory in the working tree. Add a package TestMain
that points INFER_LOGGING_DIR at a throwaway temp dir for the run, so
the logger writes there instead. No test asserts on logging.dir, and
the tests that clear INFER_* env vars already chdir into their own
temp dir, so the override is safe and self-cleaning.
@edenreich edenreich force-pushed the feat/bash-whitelist-append-env-vars branch from 9241cb5 to f382e4a Compare June 6, 2026 22:21
@edenreich edenreich changed the title feat(config): append env vars for bash whitelist feat(config): flags + append env vars for the bash whitelist Jun 6, 2026
Expose the four bash-whitelist env vars as persistent flags:
--tools-bash-whitelist-commands / --tools-bash-whitelist-patterns and
their -append siblings. For each list the replace source overrides the
resolved value and the append source merges onto it; the env var takes
precedence over the matching flag, consistent with the documented
flags < env layering.

Consolidate all whitelist resolution into applyBashWhitelistOverrides
(run after ReadInConfig so appends see config-file values), replacing
the inline env-only blocks in initConfig.
@edenreich edenreich force-pushed the feat/bash-whitelist-append-env-vars branch from 392c5f1 to 629edf5 Compare June 6, 2026 22:29
edenreich added 2 commits June 7, 2026 01:30
Tighten what the Bash tool auto-approves so it cannot exfiltrate secrets or smuggle an arbitrary tail:

- Only a single command is auto-approved; any top-level operator (|, &&, ||, ;, &) falls through to approval (operators inside quotes still count as one command). Closes the "echo x | xargs rm" prefix hole.
- $VAR may be used in commands, but expanding one in a command that prints (echo/printf) or publishes (gh issue/pr create|comment|edit) is rejected, so the agent cannot echo or post an environment variable's value.
- git push is never auto-approved by the default list.
- Command substitution, file-write redirects and dangerous find actions stay blocked; each rejection now returns an actionable hint to the model.

Behavior change: commands that previously auto-approved (compound/piped commands, echo $VAR) now require approval. CI callers can re-add specific safe commands via INFER_TOOLS_BASH_WHITELIST_COMMANDS_APPEND.
@edenreich edenreich force-pushed the feat/bash-whitelist-append-env-vars branch from 62a7a3d to b8a0fc4 Compare June 7, 2026 00:49
Comment thread cmd/root_test.go Outdated
Co-authored-by: Eden Reich <eden.reich@gmail.com>
Comment thread cmd/root_test.go Outdated
@edenreich edenreich changed the title feat(config): flags + append env vars for the bash whitelist feat(config): single-command bash whitelist + env-var leak guard Jun 7, 2026
Comment thread cmd/root_test.go Outdated
Comment thread cmd/root_test.go Outdated
Comment thread cmd/root_test.go Outdated
edenreich added 9 commits June 7, 2026 02:53
Co-authored-by: Eden Reich <eden.reich@gmail.com>
Rework PR #618's bash auto-approval into a single config-driven, per-mode
allowed-list. A bash command auto-runs only if it matches the allowed-list
for the active agent mode; anything unmatched is denied - an approval
prompt in chat, a hard reject with an actionable hint in headless agent
mode. There is no separate deny list.

- Config: tools.bash.mode.{all,plan,standard,auto}.allow. mode.all is the
  every-mode baseline, unioned with the active mode's own list
  (bashAllowFor). mode.auto defaults to ".*" (unrestricted) so headless
  `infer agent` commits and pushes without approval; mode.plan is empty.
- Matching is full-command (\A(?:entry)\z): a bare token matches only
  itself, entries opt into arguments via ( .*)?. The ".*" sentinel means
  unrestricted and skips the clean-command guard.
- Clean-command guard (every non-".*" mode) rejects command substitution,
  multi-command chains/pipelines, surviving file-write redirects, dangerous
  find actions, and env-var leaks (echo/printf/gh publish expanding $VAR).
- One matcher, IsBashCommandAllowed(command, mode), is shared by the Bash
  tool gate, the approval policy, and agent auto-approval. Mode flows via
  domain.WithAgentMode context + AgentMode.AllowedlistKey().
- The per-mode allowed-list is injected into the system prompt and rebuilt
  each turn so a chat mode toggle re-injects it.

Renames the bash auto-approval vocabulary from "whitelist" to "allowed"
across identifiers, messages, comments, and docs.

tools.bash.whitelist.commands is removed; configure
tools.bash.mode.<mode>.allow instead. The bespoke
INFER_TOOLS_BASH_WHITELIST_COMMANDS[_APPEND] env vars and
--tools-bash-whitelist-commands* flags are removed - the allowed-list is
config-file driven only.
- drop the raw `gh api` GET wildcard from defaults; the baseline now
  enumerates explicit non-destructive gh subcommands (incl. gh project
  reads), with gh api opt-in per repo.
- move read-only `gh project (list|view|item-list|field-list)` into the
  mode.all baseline; gh project writes (item-add/item-edit) are no longer
  auto-approved and fall through to approval.
- stop hardcoding the gh api regex in tests; allow-patterns come from
  DefaultConfig only.
- add `prompts.agent.system_prompt_auto`: a destructive-action policy for
  auto-accept mode (confirm/avoid irreversible ops), wired in
  getSystemPromptForMode.
- buildBashAllowInfo tells the model not to retry a denied command but to
  stop and ask the user or use an allowed alternative.
Headless `infer agent` previously defaulted to auto mode (`.*`), so CI,
heartbeat, and any run without --require-approval could execute ANY
command unattended. This makes the restricted path the default.

- add tools.safety.approval_behaviour (prompt|ipc|block, default prompt)
  deciding HOW a needed approval is delivered, alongside the existing
  require_approval which decides WHETHER. config.ResolveApprovalDelivery
  is the shared resolver; Config.Validate rejects unknown values.
- headless executor now runs in standard mode and routes approval-
  requiring tools per behaviour: IPC when a broker is attached
  (--require-approval, e.g. the channel manager), else blocked with a
  reason. No more `.*` default; full autonomy is now an explicit opt-in.
- chat honours block (rejects without prompting); prompt/ipc still prompt.
- docs, CLAUDE.md, and the seeded config document the field and the
  controlled-autonomy CI profile.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant