amplihack copilot — Subprocess-Safe Defaults¶
Issue: #621
Status: Shipped
Scope: crates/amplihack-cli — Copilot subcommand only (Codex / Amplifier subcommands unchanged)
Overview¶
amplihack copilot automatically detects when it is running as a delegated
subprocess (no controlling TTY, or invoked by another agent) and adjusts its
default behavior so that headless callers — engineer subprocesses, recipe-runner
agents, CI shell scripts — can write files, commit, and open pull requests
without permission-denied failures.
This eliminates the previously-required workaround in caller repositories of
appending --allow-all-tools --allow-all-paths to every Copilot CLI invocation
manually (see Simard PR #1720).
All callers — including future agents and human shell invocations from CI —
now get correct sandbox behavior automatically.
What "Subprocess-Safe Context" Means¶
A Copilot invocation is treated as subprocess-safe if any of the
following are true:
| Signal | Detection |
|---|---|
| Explicit user opt-in | --subprocess-safe flag passed |
| Delegated agent | AMPLIHACK_AGENT_BINARY env var is set to a non-empty value |
| Non-interactive marker | AMPLIHACK_NONINTERACTIVE=1 is set |
| Headless I/O | Any of stdin / stdout / stderr is not a TTY |
Detection happens once at dispatch time. The resolved value is propagated to all downstream code (including the docker launcher) so behavior is consistent end-to-end.
Distinct from
is_noninteractive(): Subprocess-safe detection examines all three standard streams plusAMPLIHACK_AGENT_BINARY, while the olderis_noninteractive()helper only examinedstdin. Callers ofis_noninteractive()keep their existing semantics; subprocess-safe is a separate, stricter signal scoped to the Copilot subcommand.
Behavior Changes¶
When subprocess-safe context is active, amplihack copilot automatically:
- Injects
--allow-all-toolsinto the underlyingcopilotCLI argv. - Injects
--allow-all-pathsinto the underlyingcopilotCLI argv. - Defaults
--no-reflectionto ON (suppresses reflection on a delegated session — a subprocess delegate has no value in running reflection on its own work).
When subprocess-safe context is not active (interactive TTY, no flag,
no agent-binary env), none of these granular flags are injected. The
preexisting default --allow-all injection (issue #303) is unaffected — see
Layering with --allow-all below.
Argv injection order¶
Injected flags are prepended before any user-supplied trailing args, so duplicates passed by the caller take precedence (typical CLI semantics).
Duplicate suppression¶
The injection is idempotent. If the user already passed any of the following, no duplicate is added:
--allow-all-tools→ suppresses injection of--allow-all-tools--allow-all-paths→ suppresses injection of--allow-all-paths--allow-all(broader) → suppresses injection of both granular flags
Reflection precedence¶
--reflection (new opt-in) and --no-reflection are mutually exclusive
(enforced by clap conflicts_with). The effective decision uses this
precedence:
--reflectionpassed → reflection ON (overrides everything, including subprocess-safe).--no-reflectionpassed → reflection OFF.- Subprocess-safe context active (and no explicit reflection flag) → reflection OFF (default flip).
- Otherwise → reflection ON (preexisting default).
Usage¶
Headless / CI invocation (auto-detected)¶
# stdout/stderr piped — auto-detected as subprocess-safe
amplihack copilot -p "Fix the failing test in src/auth.rs" 2>&1 | tee log.txt
No flags required. The copilot CLI receives --allow-all-tools and
--allow-all-paths automatically; reflection is suppressed.
Delegated agent invocation (env-detected)¶
# Caller sets AMPLIHACK_AGENT_BINARY to indicate this is a delegated subprocess
AMPLIHACK_AGENT_BINARY=copilot amplihack copilot -p "Implement the design spec at docs/SPEC.md"
Subprocess-safe defaults activate even on a TTY, because the env var indicates this process is acting on behalf of a parent agent.
Explicit opt-in (interactive shell)¶
# Force subprocess-safe defaults even at an interactive TTY
amplihack copilot --subprocess-safe -p "Refactor this module"
Useful when scripting against amplihack copilot from an interactive shell
and you want the headless defaults applied unconditionally.
Opt back into reflection while subprocess-safe¶
# Subprocess-safe is auto-active (CI), but you want reflection ON anyway
amplihack copilot --reflection -p "Long-running investigation task"
The --reflection flag is the only way to re-enable reflection in a
subprocess-safe context. It overrides both the auto-detected default flip and
any propagated --no-reflection.
Interactive shell, no overrides (unchanged behavior)¶
# At a real terminal, no env vars set — subprocess-safe NOT active
amplihack copilot -p "Help me explore this codebase"
The copilot CLI receives only the preexisting --allow-all default (from
issue #303). No granular --allow-all-tools / --allow-all-paths are
injected. Reflection is ON (preexisting default).
Configuration Reference¶
Flags on amplihack copilot¶
| Flag | Type | Default | Description |
|---|---|---|---|
--subprocess-safe |
bool | false |
Force subprocess-safe defaults (even on TTY). Implies --allow-all-tools, --allow-all-paths, and --no-reflection. |
--reflection |
bool | false |
Force reflection ON. Conflicts with --no-reflection. Overrides subprocess-safe default. |
--no-reflection |
bool | false |
Force reflection OFF. Conflicts with --reflection. |
(All other flags — --docker, --allow-all-paths, etc. — behave as before.)
Environment Variables¶
| Variable | Effect |
|---|---|
AMPLIHACK_AGENT_BINARY |
If set non-empty → triggers subprocess-safe context. (Set by parent agent runtimes — Claude Code, recipe-runner, Copilot CLI agent dispatch — to identify the active binary.) |
AMPLIHACK_NONINTERACTIVE |
If =1 → triggers subprocess-safe context. |
AMPLIHACK_COPILOT_NO_ALLOW_ALL |
If =1 → suppresses the preexisting --allow-all blanket injection (#303). Not weakened by subprocess-safe. (See layering below.) |
RUST_LOG=debug |
Emits a tracing::debug! line documenting the resolved subprocess-safe + reflection decision and which signals fired. |
Inspecting the decision¶
Run with RUST_LOG=debug to see the audit log:
RUST_LOG=debug amplihack copilot --subprocess-safe -p "test" 2>&1 | grep amplihack_cli
# DEBUG amplihack_cli::commands: copilot dispatch subprocess_safe_resolved=true
# explicit_flag=true agent_binary_set=false amplihack_noninteractive=false
# any_stream_non_tty=false no_reflection_effective=true
Note: The exact
tracing::debug!line format above reflects the implemented field names (subprocess_safe_resolved,explicit_flag,agent_binary_set,amplihack_noninteractive,any_stream_non_tty,no_reflection_effective). The contract is that the resolved decision and all four input signals are observable atdebuglevel — string layout may evolve.
Layering with --allow-all¶
amplihack copilot already injected the blanket --allow-all flag for every
invocation by default since #303.
That behavior is unchanged by this feature.
When subprocess-safe is also active, the resulting copilot argv contains
both the blanket --allow-all and the granular --allow-all-tools /
--allow-all-paths. This is intentional, defense-in-depth, and accepted by
the copilot CLI without conflict. The granular flags satisfy the literal
contract of subprocess-safe (so callers can audit argv for the specific
tokens) without disturbing the broader --allow-all default.
| Context | --allow-all |
--allow-all-tools |
--allow-all-paths |
|---|---|---|---|
| Interactive TTY (no flag, no env) | ✅ (preexisting #303) | ❌ | ❌ |
| Subprocess-safe active | ✅ (preexisting #303) | ✅ (new) | ✅ (new) |
Subprocess-safe + AMPLIHACK_COPILOT_NO_ALLOW_ALL=1 |
❌ (opt-out) | ❌ (opt-out) | ❌ (opt-out) |
User passed --allow-all themselves |
✅ (user) | ❌ (suppressed by superset) | ❌ (suppressed by superset) |
Docker Mode¶
--subprocess-safe is propagated through build_docker_launcher_args.
Resolution happens at the outer amplihack-cli layer before docker dispatch,
so the resolved decision (including any auto-detection result) is carried
into the container. No additional configuration is needed inside the docker
image.
# Auto-detection fires on the host; flag is propagated into the container
AMPLIHACK_AGENT_BINARY=copilot amplihack copilot --docker -p "Run the tests"
Examples¶
CI script that delegates to amplihack copilot¶
# .github/workflows/agent.yml
- name: Run amplihack copilot agent
env:
AMPLIHACK_AGENT_BINARY: copilot # Mark as delegated
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
amplihack copilot -p "Fix the issue described in $ISSUE_BODY"
# No --allow-all-tools / --allow-all-paths needed — auto-injected.
# No --no-reflection needed — auto-defaulted.
Engineer subprocess (replaces Simard PR #1720 workaround)¶
Before:
// Simard engineer/launcher.rs (workaround)
let argv = vec![
"amplihack", "copilot",
"--allow-all-tools", // <-- workaround
"--allow-all-paths", // <-- workaround
"--no-reflection", // <-- workaround
"-p", &task,
];
After:
// Simard engineer/launcher.rs (cleanup)
std::env::set_var("AMPLIHACK_AGENT_BINARY", "copilot");
let argv = vec!["amplihack", "copilot", "-p", &task];
// All three flags now auto-applied by amplihack-rs.
Note: The Simard workaround removal is a separate follow-up PR in that repository. The change in amplihack-rs is backward-compatible — existing callers that pass the granular flags explicitly continue to work (duplicate suppression handles them).
Edition footnote:
std::env::set_varisunsafeunder the Rust 2024 edition. The wrapping required at the call site (unsafe { ... }block, or a safer alternative such as setting the env var in the parent process before spawn) is determined by the consuming crate's edition — Simard's own toolchain dictates the exact form. amplihack-rs only reads the env var; it does not constrain how callers write it.
Recipe-runner subprocess agent¶
# Inside a recipe step
amplihack recipe run my-workflow -c agent_binary=copilot
# Internally launches `amplihack copilot ...` with AMPLIHACK_AGENT_BINARY=copilot;
# subprocess-safe defaults activate automatically.
Migration Guide¶
For amplihack callers (Simard, recipes, CI scripts)¶
If your code currently appends --allow-all-tools, --allow-all-paths, or
--no-reflection to amplihack copilot invocations as a workaround:
- Set
AMPLIHACK_AGENT_BINARY=copilotbefore invoking, OR pass--subprocess-safeexplicitly. - Remove the workaround flags from your argv (they are now redundant — though keeping them is harmless thanks to duplicate suppression).
- Verify with
RUST_LOG=debugthatsubprocess_safe_resolved=trueand the granular flags appear in the launchedcopilotargv.
For interactive amplihack copilot users¶
No action required. Interactive TTY behavior is unchanged. The new defaults only fire when at least one subprocess-safe signal is present.
For users who want the new flags in interactive shells¶
Pass --subprocess-safe (or set AMPLIHACK_NONINTERACTIVE=1 / use a non-TTY
shell wrapper).
Out of Scope¶
This feature does not:
- Modify the
copilotCLI itself (no upstream changes). - Modify the Codex or Amplifier subcommands (Copilot subcommand only; trivial extension if requested in a follow-up).
- Modify the auto-mode helper path (
commands/auto_mode/helpers.rs:214), which already injects--allow-allseparately. Documented as a follow-up. - Introduce any new sandbox / permission models — it composes existing
copilotCLI flags. - Add telemetry beyond a single
tracing::debug!decision audit at the dispatch boundary.
Security Considerations¶
- No new attack surface introduced. The preexisting
--allow-alldefault (#303) already runscopilotwith full permissions in every interactive context. Subprocess-safe adds redundant granular flags only in subprocess contexts where, by definition, no human is supervising stdio prompts anyway. The granular flags do not relax any sandbox boundary that--allow-allwas already opening. AMPLIHACK_COPILOT_NO_ALLOW_ALL=1opt-out is honored across the board. The hardened-operator opt-out suppresses the broader--allow-alland the granular--allow-all-tools/--allow-all-paths. An operator who has explicitly disabled amplihack auto-permissioning of copilot keeps that posture even when subprocess-safe auto-detects. (See layering above.)- Trust model unchanged. Anyone who can set
AMPLIHACK_AGENT_BINARYor redirect stdio already controls process startup; subprocess-safe inherits that trust posture, never escalates it. - Reflection auto-disable is a safety improvement. Prevents nested infinite recursion when amplihack invokes itself (the bug that motivated issue #621).
- No
unsafeblocks; nounwrap()on env reads; no runtime-derived argv tokens. The injected flag tokens are compile-time&'static strliterals.
Migration note: reflection in piped / CI contexts¶
If you previously relied on amplihack copilot running its post-session
reflection pass in a non-TTY context (CI logs, piped stdout, tee, nohup,
…), be aware that reflection now defaults off when subprocess-safe
auto-detects. To restore the prior behavior, pass --reflection explicitly:
See Also¶
COPILOT_CLI.md— Full Copilot integration overviewAUTOMODE_SAFETY.md— Automode safety guide- Issue #621 — Source issue
- Issue #303 — Preexisting
--allow-alldefault - Simard PR #1720 — Original workaround (now removable)