Skip to content

adapter.extra_args replaces bypass_args rather than extending it, so setting it for any other flag silently drops the permission bypass #740

Description

@pbean

Found while validating #727, where it is the most plausible root cause of the reported incident. Validated against main @ cf5d5b99; every cite below was resolved by symbol.

The behavior

GenericAdapter.interactive_argv (src/bmad_loop/adapters/generic.py:513-522):

def interactive_argv(self, spec: SessionSpec) -> list[str]:
    extra = self.extra_args
    if extra is None:
        extra = self.profile.bypass_args
    argv = [self.binary, *self.profile.launch_args, self.profile.render_prompt(spec.prompt), *extra]

extra_args is not extra. It is a total replacement for bypass_args, and bypass_args is the field that carries the permission bypass on every shipped profile:

profile bypass_args
claude.toml:5 ["--permission-mode", "bypassPermissions"]
codex.toml:10 ["--dangerously-bypass-approvals-and-sandbox"]
gemini.toml:10 ["--approval-mode=yolo"]
copilot.toml:24 ["--allow-all-tools", "--allow-all-paths"]
antigravity.toml:39 ["--dangerously-skip-permissions"]

So an operator who sets extra_args for an unrelated reason — a model flag, an --add-dir, a debug switch — silently launches every session of that stage without a permission bypass. The unattended loop's first tool call then parks on a confirmation prompt nobody is present to answer.

Why the blast radius is wider than one stage

Stage inheritance carries a base-level extra_args down to every stage running the same client (src/bmad_loop/policy.py:410-423): a stage's own extra_args, else the base's when the client matches, else None. So one [adapter] extra_args line reaches dev, review and sweep-triage together.

And a session that parks on a prompt is not distinguishable from one that is thinking: it takes the stalled path, which RETRIES while budget lasts (escalation.py:134-148), so the failure spends attempts rather than announcing itself.

This is not a documentation gap

It is documented, in four places:

  • policy.py:1353-1354 — the generated policy.toml template comment: "extra_args replaces the profile's default permission-bypass flags when set", with the claude bypass pair as the worked example.
  • README.md:441-442 — the same.
  • docs/tui-guide.md:677-680 — the settings-editor switch distinguishes "use the profile's default flags" from "replace them with exactly this list".
  • runsetup.py:178-182 and :210-213 — states it twice while reasoning about the config digest, including the consequence: "for a role that sets it the hashed bypass_args is dead".

The defect is that a documented footgun has no guard. Nothing at policy load, validate, probe, or run start notices that the effective launch argv for a role now contains no bypass at all.

One place the documentation genuinely does not reach is the TUI settings editor, which is where an operator is most likely to set this without reading the template comment: src/bmad_loop/data/settings/core.toml:222-224 (and :258-260 for the per-stage table) declare extra_args with kind = "args" and no placeholder — while its sibling model two lines up carries one.

What is being asked for

A warning, not a semantic change. The replace semantics should stay: None ("inherit the profile") and () ("explicitly no flags at all") are deliberately distinct states, kept apart on purpose through the config digest (runsetup.py:180-182, runsetup.py:401-403), and an operator who genuinely wants a bare launch is entitled to it.

Suggested shape — warn when the resolved extra_args for a role is non-None and shares no token with that profile's bypass_args:

  • as a warn finding in bmad-loop validate / probe, alongside the existing adapter.binary-unrunnable check (cli.py:557);
  • and/or a one-line journal note at session construction naming the role and the flags that were displaced.

A placeholder on the two core.toml fields carrying the same sentence as the template comment would close the TUI half cheaply and independently.

Related to #727, whose reported symptom — a claude session on a confirmation screen with tokens: 0 — is what this produces. It also compounds with #739: a session parked on that prompt is silent, and the wake nudge types into the prompt.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Real defect - workaround exists or blast radius is narrowarea:adaptersCoding-CLI adapters and profilesarea:configBMAD config resolution and the setup skillbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions