Skip to content

ACP/non-interactive auth can rewrite durable security.auth.selectedType from OAuth to API-key auth when GEMINI_API_KEY is ambient #25687

@anyech

Description

@anyech

Summary

We observed a surprising auth-state mutation in Gemini CLI during an ACP / non-interactive flow.

In an isolated HOME with durable Gemini auth pinned to Google sign-in (oauth-personal), a risky ACP path with ambient GEMINI_API_KEY was able to drift the saved auth selection away from OAuth and into the API-key lane.

The concerning part is not only the runtime fallback. The ACP/non-interactive flow appears able to persist the fallback auth method back into durable local settings for later runs.

I am reporting this as observed behavior that may be unintended. If this is actually expected for ACP/non-interactive flows, then I think there should at least be a way to opt out of durable auth mutation.

Version / environment

  • Observed on Gemini CLI v0.38.2
  • v0.38.2 is also the current latest GitHub release at the time of filing
  • Surface: ACP / non-interactive execution
  • Prior durable auth state in the isolated HOME:
    • ~/.gemini/settings.json
      • security.auth.selectedType = oauth-personal
      • security.auth.enforcedType = oauth-personal
    • valid cached Google account / OAuth credential files present

What we observed

We ran two isolated-HOME stage cases without touching the live ~/.gemini directory.

Case A, hardened path

  • durable Gemini settings pinned to oauth-personal
  • an outer launcher kept explicit selection pressure toward the OAuth lane
  • GEMINI_API_KEY was still intentionally injected into the staged process

Result:

  • the staged call did not complete successfully, but the durable auth state remained pinned to oauth-personal
  • no auth drift happened

Case B, risky path

  • removed the outer selection guard
  • launched Gemini through a bare ACP path with ambient GEMINI_API_KEY

Result:

  • after the staged run, the isolated ~/.gemini/settings.json had drifted to selectedType = gemini-api-key
  • the staged OAuth/account lane was no longer the active durable lane for subsequent runs

This makes the problem look like credential precedence plus durable state mutation, not just an expired cache.

Minimal reproduction shape

I do not yet have a pure-gemini-cli-only repro without an ACP client, but the minimal observed shape was:

  1. Create an isolated HOME
  2. Stage ~/.gemini/settings.json with:
    • selectedType = oauth-personal
    • enforcedType = oauth-personal
  3. Stage valid cached OAuth/account files in that isolated HOME
  4. Export GEMINI_API_KEY into the environment
  5. Start Gemini CLI through an ACP/non-interactive path that does not independently pin the OAuth lane
  6. Execute one non-interactive prompt turn
  7. Inspect the isolated ~/.gemini/settings.json

Observed result in the risky path:

  • security.auth.selectedType changed from oauth-personal to gemini-api-key

The exact helper I used on my side was an external ACP client. If helpful, I can provide a smaller reproduction packet for that path.

Why this seems surprising

From the docs, security.auth.selectedType reads as the durable selected auth method, and security.auth.enforcedType reads as the required auth method:

  • docs/reference/configuration.md

I did not find an obvious note in the README/configuration docs saying that ACP/non-interactive fallback auth is expected to rewrite the durable auth selection based on ambient env credentials.

Source seams that look relevant

I realize bundled code can differ from source, so below are the corresponding source-level seams from main that appear relevant.

1. ACP authenticate persists the chosen method into durable settings

packages/cli/src/acp/acpClient.ts

async authenticate(req: acp.AuthenticateRequest): Promise<void> {
  const { methodId } = req;
  const method = z.nativeEnum(AuthType).parse(methodId);
  ...
  await this.context.config.refreshAuth(method, apiKey, baseUrl, headers);
  this.settings.setValue(
    SettingScope.User,
    'security.auth.selectedType',
    method,
  );
}

2. Non-interactive auth validation allows env discovery into the effective auth type

packages/cli/src/validateNonInterActiveAuth.ts

const effectiveAuthType = configuredAuthType || getAuthTypeFromEnv();

Those two together look risky in ACP/non-interactive flows:

  1. env makes the API-key lane eligible
  2. ACP authenticates with that method
  3. durable selectedType gets rewritten

Expected behavior

One of these would feel safer:

  1. ACP/non-interactive fallback auth should not rewrite durable security.auth.selectedType, or
  2. only explicit user-driven auth-selection flows should persist selectedType, or
  3. ACP/non-interactive flows should require an explicit opt-in before changing durable auth preference, or
  4. if this behavior is intentional, there should be a documented auth-lock / do-not-persist mechanism for mixed OAuth + API-key environments

Questions

  • Is this durable auth rewrite in ACP/non-interactive flows intentional?
  • If yes, would you accept a feature request for an auth-lock / no-persist mode?
  • If no, does the acpClient.ts persistence path look like the right fix seam?

Thanks. I can provide more exact before/after state and the ACP-side repro details if that would help narrow this further.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status/need-triageIssues that need to be triaged by the triage automation.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions