recommended_plugins injection has no working opt-out (feature flag removed / disable-plugins is all-or-nothing)

Open 💬 5 comments Opened Aug 12, 2026 by konomings
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

The <recommended_plugins> block (list of ~38 not-installed third-party plugins, e.g. Atlassian, Airtable, HubSpot, ...) is unconditionally injected as an input_text item on the first user turn of every session, and there is no working client-side way to suppress it — even though a related feature-flag mechanism exists.

Environment

  • codex-cli 0.146.0 (Windows, native CLI)
  • Also reproduced earlier on 0.147.0-alpha.6.5

Steps to Reproduce (no API billing — uses local prompt rendering only)

codex debug prompt-input "test"

Output contains a developer/user message with:

"text": "<recommended_plugins>\nHere is a list of plugins that are available but not installed.\n\n- Airtable (airtable@openai-curated-remote)\n- Apollo.io (apollo@openai-curated-remote)\n...\n</recommended_plugins>"

~1,900 characters / ~470 tokens, listing ~38 plugins from a remote/hardcoded catalog (@openai-curated-remote) that is unrelated to the local codex plugin marketplace list configuration.

What I tried (all ineffective)

  1. codex features list no longer even lists a recommended_plugins flag (it did on the alpha build tested earlier, shown as stable false). Running:

``
codex debug prompt-input --disable recommended_plugins "test"
`
now fails with
Error: Unknown feature flag: recommended_plugins. On the earlier alpha build where the flag still existed, passing --disable recommended_plugins` produced byte-identical output to the default run (only a random message ID differed) — i.e. the flag never actually controlled this injection path even when it was listed.

  1. --disable plugins (the only remaining related flag, stable true by default) does remove the <recommended_plugins> block, but it does so by disabling the entire plugin instruction subsystem — it also strips the <plugins_instructions> block that documents currently-installed and enabled plugins. For a user with real plugins enabled (e.g. documents, spreadsheets, chrome, computer-use, pdf, ...), this is not usable: it silences the model's knowledge of plugins actually in use, not just the unused recommendations list.
  1. Emptying the plugin marketplace config (-c "marketplaces={}", process-local, non-destructive) does not change the injected list at all — confirming the list is generated from a source independent of local [marketplaces] config.

Impact

Because history_mode: "legacy" resends full conversation history on every API call within a session, this ~470-token block is effectively billed on every turn of the session, not just the first — a constant, unavoidable per-session tax with no opt-out.

Expected behavior

Either:

  • features.recommended_plugins (or an equivalent, currently-functional flag) actually gates this injection, independent of features.plugins, or
  • the recommendation list is omitted/trimmed after the first turn (respecting history_mode), or
  • there is a config key (e.g. show_recommended_plugins = false) to opt out without disabling real plugin functionality.

Additional notes

  • Searched existing issues for recommended_plugins / "recommended plugin injection" and found no exact duplicate as of this report.
  • Happy to provide the full codex debug prompt-input output (local rendering, no session/user data) if useful.

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 15 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #37024

Powered by Codex Action

ronald0samuel · 13 days ago

yeah same issue here, please fix this annoying issue. I've mentioned it in thid feedback id 01a00523-4b21-76b1-aac9-e44951a2d45b

cosentinode · 11 days ago

Root-cause analysis on current main (I traced the injection path end-to-end). The short version: the bug you're hitting is still present on main today, but the "feature flag removed" part needs a correction — the flag exists now, it just doesn't gate this injection.

Status of the flag

features.recommended_plugins was added on Jul 28 in #35839 ("Decouple recommended plugins from tool suggestions") as Stage::Stable with default_enabled: false (codex-rs/features/src/lib.rs:1183). So on current main codex features list does list it, and --disable recommended_plugins no longer errors with "Unknown feature flag". Your 0.146.0 predates #35839, which explains the "Unknown feature flag" error; your alpha build was the first to carry the flag.

Why --disable recommended_plugins still does nothing

The recommended_plugins flag is wired up as an OR, not a gate. In build_initial_context_with_world_state (codex-rs/core/src/session/mod.rs:3501):

if features.enabled(Feature::Apps)
    && features.enabled(Feature::Plugins)
    && (features.enabled(Feature::ToolSuggest)
        || features.enabled(Feature::RecommendedPlugins))
  • apps, plugins, and tool_suggest are all Stage::Stable with default_enabled: true (features/src/lib.rs:1178).
  • recommended_plugins defaults to false.

So with default config, the first turn always satisfies the gate via tool_suggest alone, and disabling only recommended_plugins cannot suppress the block — exactly the byte-identical output you observed on the alpha. Your --disable plugins finding is also consistent: plugins short-circuits the entire gate, which is why it also removes the <plugins_instructions> block that documents actually-installed plugins.

Where the ~38 entries come from

Confirmed server-side and config-independent, as you suspected. fetch_recommended_plugins (codex-rs/core-plugins/src/remote.rs:993) does an authenticated GET {chatgpt_base_url}/ps/plugins/suggested?scope=GLOBAL and only falls back to a Legacy mode when the response says enabled != true (remote.rs:1009) or the request fails — which is also why emptying marketplaces locally has no effect. In Legacy mode recommended_plugin_candidates_for_config returns None (codex-rs/core-plugins/src/manager.rs:1340), so no block is rendered. The rendered list is built by RecommendedPluginsInstructions (codex-rs/core/src/context/recommended_plugins_instructions.rs), capped at 50 entries.

The per-turn billing concern

build_initial_context_with_world_state runs when there is no reference context to diff against — the first turn, after compaction, or on resume. Within a session with history_mode: "legacy" the full thread is resent on every API call, so the block is indeed re-billed each turn.

Fix directions to consider

  1. Gate the injection on Feature::RecommendedPlugins specifically (not the OR), keeping request_plugin_install gated by tool_suggest as it is today (core/src/tools/handlers/request_plugin_install_spec.rs). The flag was added in #35839 specifically to decouple the recommendation list from tool suggestions; as wired, the decoupling only works in one direction (you can enable the list with tool_suggest off, but you cannot disable the list with tool_suggest on).
  2. Respect explicit false for the flag as a veto: if the feature layer can distinguish "explicitly disabled" from "default-disabled", treat recommended_plugins = false as a hard opt-out even when tool_suggest is enabled.
  3. Trim after the first turn (only inject when the reference context is first established, and exclude it from legacy history replay).

For reference, the existing coverage is codex-rs/app-server/tests/suite/v2/recommended_plugins.rs (both tool_suggest on/off paths assert the block is present), so a fix would need to update those assertions. Happy to help with a proposed patch if any of these directions aligns with the team's intended solution.

maxivillus · 11 days ago

Comment: openai/codex #38185 — delta from our duplicate #38881

---

Same bug on our side (codex-cli 0.147.0, Linux, app-server mode) — my report
#38881 is a duplicate of this one and is being closed as such. Two observations
not in the report above:

  1. config.toml quirk. features.* keys placed after a [table] section in

config.toml are silently ignored; the same key before the table takes
effect. So "I set recommended_plugins=false in config" can fail twice: the
flag isn't wired to this injection path, and depending on file layout the
value never parses. Reproduced by moving the key across a [table] boundary
in the same file (0.147.0).

  1. A/B of the only working lever. features.plugins=false (or

--disable plugins) removes <recommended_plugins>, but it also removes
the <plugins_instructions> block that documents actually-installed
plugins — confirmed by diffing codex debug prompt-input output. For users
with real plugins enabled that opt-out is unusable, which is the
"all-or-nothing" point.

Cost anchor: ~1.9 KB ≈ ~0.5K tokens per turn, re-emitted on every turn; with
history_mode: "legacy" the fragment is resent with the full conversation, so
long sessions pay it repeatedly.

jdcodes1 · 9 days ago

Found why your flag attempts were ineffective (main @ 1f41cc5d92). The injection gate is:

features.enabled(Feature::Apps)
    && features.enabled(Feature::Plugins)
    && (features.enabled(Feature::ToolSuggest) || features.enabled(Feature::RecommendedPlugins))

(core/src/session/mod.rs#L3505-L3509). recommended_plugins is default-off — but tool_suggest is default-on (features/src/lib.rs#L1178-L1188), and the || means ToolSuggest alone keeps the block flowing. So -c features.recommended_plugins=false is a no-op in practice, and the only levers that work are the all-or-nothing ones you found (plugins/apps off).

Working opt-out today: -c features.tool_suggest=false (with recommended_plugins left at its default false) makes the whole condition false — worth verifying with your codex debug prompt-input check, and it also disables tool-suggest generally, so it's still coarser than you asked for.

Fix shape: the || conflates two features. Make recommended_plugins the sole gate for the <recommended_plugins> block (ToolSuggest can imply it as a default, not an override), or honor an explicit recommended_plugins=false as a veto. Either restores a targeted opt-out for a ~470-token-per-session injection users can't otherwise control.