config.toml migration generates a permissions.<name> block that fails --strict-config parsing; sandbox_workspace_write.network_access silently ignored in static config

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

What version of Codex CLI is running?

codex-cli 0.149.1

What platform is your computer?

Linux (Ubuntu-based), x86_64

What issue are you seeing?

After npm install -g @openai/codex@latest auto-migrated my ~/.codex/config.toml, it wrote a default_permissions = "protect-env" block using the newer [permissions.<name>] profile system:

default_permissions = "protect-env"

[permissions.protect-env]
extends = ":workspace"

[permissions.protect-env.filesystem]
glob_scan_max_depth = 8

[permissions.protect-env.filesystem.":workspace_roots"]
"**/*.env" = "deny"

Running with --strict-config against this exact, unmodified, migration-generated config fails to parse:

Error loading config.toml:
/home/amlan/.codex/config.toml:8:2: data did not match any variant of untagged enum FilesystemPermissionToml
  |
8 | [permissions.protect-env]
  |  ^^^^^^^^^^^

Without --strict-config, this same block is silently ignored — no error, but default_permissions = "protect-env" appears to be a no-op, since sandboxed sessions always fall back to the stock :workspace sandbox defaults (.git read-only, network access disabled) regardless of the profile's contents.

Separately (possibly related, possibly a separate default-change from an update): setting the older/documented

[sandbox_workspace_write]
network_access = true
writable_roots = ["/some/path"]

at the config.toml root (or via a -p profile file) is also silently ignored — no error, no effect on the sandbox's writable roots or network access shown in the session banner. The equivalent -c sandbox_workspace_write.network_access=true CLI override does work at runtime, so there's a discrepancy between how this key is honored via static config-file loading vs. a -c override.

What steps can reproduce the bug?

  1. Have Codex CLI auto-update to 0.149.1 (or otherwise obtain a config.toml with a migration-generated [permissions.<name>] block, e.g. via the protect-env deny-.env migration).
  2. Run any command with --strict-config, e.g.:

``
codex exec --strict-config -c 'default_permissions="protect-env"' "echo hi"
``

  1. Observe the parse error on the exact block the migration itself generated:

``
data did not match any variant of untagged enum FilesystemPermissionToml
``

  1. Separately, confirm sandbox_workspace_write.network_access = true set in config.toml (root level or via -p <profile>.config.toml) has no effect — git fetch/curl inside the sandbox still fail with Could not resolve host, and the session banner doesn't show (network access enabled). The same setting via -c sandbox_workspace_write.network_access=true on the command line does work.

Impact

Since the mismatch is silently swallowed outside --strict-config, users have no visibility that their permissions profile isn't applying — sandboxed sessions just behave as if no custom profile exists, with no error or warning. Given .git is read-only and network is disabled by default under :workspace, this makes basic git fetch/git push/web-search workflows fail with no indication that the intended permissions override never took effect.

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 4 days ago

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

  • #39996

Powered by Codex Action

AmlanMishra2004 · 3 days ago

Not a duplicate of #39996 — that report is about the remote ChatGPT desktop app (macOS→SSH, app-server) ignoring writable_roots for newly created remote tasks. This issue is about the local codex CLI: the auto-migrated [permissions.<name>] block in config.toml fails to parse against FilesystemPermissionToml under --strict-config (silently swallowed otherwise), and sandbox_workspace_write.network_access set statically in config has no effect while the equivalent -c override works. Different component (CLI config parsing vs. remote app-server mount handling), different repro. Keeping open.

AmlanMishra2004 · 3 days ago

Correction after re-testing against the actual source and a clean local repro:

Part 1 (--strict-config schema-mismatch parse error) does NOT reproduce. I re-ran the exact repro command from the issue body against the unmodified config multiple times (codex-cli 0.149.1, clean exit, no error). I also diffed codex-rs/config/src/permissions_toml.rs between the rust-v0.149.1 tag and current main — identical — and confirmed the FilesystemPermissionToml schema (Access(FileSystemAccessMode) | Scoped(BTreeMap<String, FileSystemAccessMode>)) does accept the config shape from the issue. I believe this part of the original report was a testing mistake on my end (likely cross-contamination from editing the config file around the same time) — retracting it. Apologies for the noise.

Part 2 (static sandbox_workspace_write.network_access silently ignored) is real, but the root cause is different from what I described. It's not a parsing/config-loading bug — it's a precedence gap: in codex-rs/core/src/config/mod.rs (around the block handling profiles_are_active), legacy [sandbox_workspace_write] settings are only merged in when using_implicit_builtin_profile is true, i.e. when default_permissions is unset. As soon as you name any custom profile via default_permissions — even one with extends = ":workspace" — the legacy block is dropped entirely with no warning or error. So this is really: legacy [sandbox_workspace_write] config has no effect once a named [permissions.<name>] profile is selected, and there's no diagnostic telling the user their setting was ignored. Narrowing the issue to that.

AmlanMishra2004 · 3 days ago

Root cause for the second part of this report (network_access silently ignored)

Found it in codex-rs/core/src/config/mod.rs, in the profiles_are_active branch of Config construction: legacy [sandbox_workspace_write] settings are only merged in when using_implicit_builtin_profile is true, i.e. when default_permissions is unset entirely. The moment default_permissions names any profile — including a custom one that extends = ":workspace" (which is exactly what the config-migration writes) — builtin_workspace_write_settings is set to None and the legacy block is dropped, silently, with no diagnostic.

This matters because the repo's own docs (codex-rs/skills/src/assets/samples/imagegen/references/codex-network.md) and the TypeScript SDK README both document [sandbox_workspace_write] network_access = true as the way to enable network access, with no mention that it stops working once a named permissions profile exists. So a user who follows the documented example, on a config the tool itself auto-migrated to use a named profile, gets no network access and no explanation — git fetch/curl/etc. just fail as if DNS is broken.

Proper fix for users hitting this today: move the setting into the new syntax instead:

[permissions.<name>.network]
enabled = true

(network.mode does not do this — I initially got this wrong; the field that actually flips NetworkSandboxPolicy in compile_network_sandbox_policy is network.enabled.)

Suggested fix

Per docs/contributing.md, posting this as analysis/suggested fix rather than a PR. Adds a startup_warnings entry (same mechanism already used for e.g. invalid theme names) when legacy [sandbox_workspace_write] settings are present but ignored due to an active named profile. No behavior change — diagnostics only.

Scoped to avoid two failure modes I found in review:

  • Does not fire when default_permissions is unset, or when it's explicitly set to the built-in :workspace (existing code already treats explicit :workspace selection as an intentional legacy opt-out — see the comment at the active_permission_profile computation a few lines below).
  • The network-specific note only appears when network_access is the field actually set, and is phrased as a fact ("this key has no effect under this profile") rather than a claim that fetch/push will fail — since the active profile may already grant network access another way (e.g. :danger-full-access), in which case nothing is actually broken.

Also adds one clarifying line + example to codex-network.md pointing at the correct new-syntax key, since that's the doc that currently sends users down the broken path.

Verified against the current main branch (e3609f2): cargo test -p codex-core --lib config:: (494 tests) passes, cargo clippy -p codex-core --lib --tests clean, cargo fmt -p codex-core -- --check clean. Diff below (not run against the full workspace test suite, so please treat as a starting point rather than a merge-ready patch):

diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs
index b02140c..fd95394 100644
--- a/codex-rs/core/src/config/config_tests.rs
+++ b/codex-rs/core/src/config/config_tests.rs
@@ -1351,6 +1351,115 @@ async fn runtime_config_uses_tui_raw_output_mode() {
     assert!(cfg.tui_raw_output_mode);
 }
 
+#[tokio::test]
+async fn legacy_sandbox_workspace_write_warns_when_named_profile_active() {
+    let toml = r#"
+default_permissions = "protect-env"
+
+[permissions.protect-env]
+extends = ":workspace"
+
+[sandbox_workspace_write]
+network_access = true
+    "#;
+    let cfg_toml: ConfigToml = toml::from_str(toml).expect("deserialize config");
+    let cfg = Config::load_from_base_config_with_overrides(
+        cfg_toml,
+        ConfigOverrides::default(),
+        tempdir().expect("tempdir").abs(),
+    )
+    .await
+    .expect("load config");
+
+    assert!(
+        cfg.startup_warnings.iter().any(|warning| warning
+            .contains("`[sandbox_workspace_write]` settings in config.toml are ignored")
+            && warning.contains("network_access")),
+        "expected a warning about ignored legacy sandbox_workspace_write settings, got: {:?}",
+        cfg.startup_warnings
+    );
+}
+
+#[tokio::test]
+async fn legacy_sandbox_workspace_write_warning_omits_network_note_for_unrelated_fields() {
+    let toml = r#"
+default_permissions = "protect-env"
+
+[permissions.protect-env]
+extends = ":workspace"
+
+[sandbox_workspace_write]
+exclude_slash_tmp = true
+    "#;
+    let cfg_toml: ConfigToml = toml::from_str(toml).expect("deserialize config");
+    let cfg = Config::load_from_base_config_with_overrides(
+        cfg_toml,
+        ConfigOverrides::default(),
+        tempdir().expect("tempdir").abs(),
+    )
+    .await
+    .expect("load config");
+
+    assert!(
+        cfg.startup_warnings.iter().any(|warning| warning
+            .contains("`[sandbox_workspace_write]` settings in config.toml are ignored")
+            && !warning.contains("network_access")),
+        "expected a warning without the network-specific note, got: {:?}",
+        cfg.startup_warnings
+    );
+}
+
+#[tokio::test]
+async fn legacy_sandbox_workspace_write_no_warning_for_explicit_builtin_workspace_profile() {
+    let toml = r#"
+default_permissions = ":workspace"
+
+[sandbox_workspace_write]
+network_access = true
+    "#;
+    let cfg_toml: ConfigToml = toml::from_str(toml).expect("deserialize config");
+    let cfg = Config::load_from_base_config_with_overrides(
+        cfg_toml,
+        ConfigOverrides::default(),
+        tempdir().expect("tempdir").abs(),
+    )
+    .await
+    .expect("load config");
+
+    assert!(
+        !cfg.startup_warnings
+            .iter()
+            .any(|warning| warning.contains("`[sandbox_workspace_write]`")),
+        "explicitly selecting `:workspace` is documented as intentionally opting out of legacy \
+         settings, so no warning should fire, got: {:?}",
+        cfg.startup_warnings
+    );
+}
+
+#[tokio::test]
+async fn legacy_sandbox_workspace_write_no_warning_without_named_profile() {
+    let toml = r#"
+[sandbox_workspace_write]
+network_access = true
+    "#;
+    let cfg_toml: ConfigToml = toml::from_str(toml).expect("deserialize config");
+    let cfg = Config::load_from_base_config_with_overrides(
+        cfg_toml,
+        ConfigOverrides::default(),
+        tempdir().expect("tempdir").abs(),
+    )
+    .await
+    .expect("load config");
+
+    assert!(
+        !cfg.startup_warnings
+            .iter()
+            .any(|warning| warning.contains("`[sandbox_workspace_write]`")),
+        "did not expect a legacy sandbox_workspace_write warning when no named profile is active, got: {:?}",
+        cfg.startup_warnings
+    );
+}
+
 #[test]
 fn config_toml_deserializes_permission_profiles() {
     let toml = r#"
diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs
index 0d0ac94..e190abc 100644
--- a/codex-rs/core/src/config/mod.rs
+++ b/codex-rs/core/src/config/mod.rs
@@ -46,6 +46,7 @@ use codex_config::types::ModelAvailabilityNuxConfig;
 use codex_config::types::Notice;
 use codex_config::types::OAuthCredentialsStoreMode;
 use codex_config::types::ResumeCwdMode;
+use codex_config::types::SandboxWorkspaceWrite;
 use codex_config::types::SessionPickerViewMode;
 use codex_config::types::ToolSuggestConfig;
 use codex_config::types::ToolSuggestDisabledTool;
@@ -3492,6 +3493,19 @@ impl Config {
             let builtin_workspace_write_settings = if using_implicit_builtin_profile {
                 cfg.sandbox_workspace_write.as_ref()
             } else {
+                if default_permissions != BUILT_IN_WORKSPACE_PROFILE
+                    && let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
+                    && *sandbox_workspace_write != SandboxWorkspaceWrite::default()
+                {
+                    let network_note = if sandbox_workspace_write.network_access {
+                        " (including `network_access`, which does not enable network access under this profile)"
+                    } else {
+                        ""
+                    };
+                    startup_warnings.push(format!(
+                        "`[sandbox_workspace_write]` settings in config.toml are ignored while the `{default_permissions}` permissions profile is active{network_note}. Move these settings into `[permissions.{default_permissions}]` instead (e.g. `network.enabled`, `workspace_roots`)."
+                    ));
+                }
                 None
             };
             let configured_network_proxy_config = network_proxy_config_for_profile_selection(
diff --git a/codex-rs/skills/src/assets/samples/imagegen/references/codex-network.md b/codex-rs/skills/src/assets/samples/imagegen/references/codex-network.md
index 5ce1fbc..57c0681 100644
--- a/codex-rs/skills/src/assets/samples/imagegen/references/codex-network.md
+++ b/codex-rs/skills/src/assets/samples/imagegen/references/codex-network.md
@@ -11,6 +11,12 @@ The fallback CLI uses the OpenAI Image API, so it needs outbound network access.
 - `--ask-for-approval never` suppresses approval prompts.
 - It does **not** by itself enable network access.
 - In `workspace-write`, network access still depends on your Codex configuration (for example `[sandbox_workspace_write] network_access = true`).
+- `[sandbox_workspace_write]` only applies when no named `default_permissions` profile is active. If your config sets `default_permissions = "<name>"`, enable network under that profile instead:
+  ```toml
+  [permissions.<name>.network]
+  enabled = true
+  ```
+  A startup warning is shown when the legacy `[sandbox_workspace_write]` key is ignored this way.
 
 ## How do I reduce repeated approval prompts?
 If you trust the repo and want fewer prompts, use a configuration or profile that both:
naipi11 · 3 days ago

If a named default_permissions profile is active, the legacy [sandbox_workspace_write] network_access = true setting may be ignored silently.

Use the setting inside the active permission profile instead:

[permissions.<name>.network]
enabled = true

Then restart Codex and retry the same network operation. Do not substitute network.mode for network.enabled; they are different fields.

Limitation: this applies only when a named permission profile is selected. If no named profile is active, the legacy workspace-write setting may still be the applicable path. This is a configuration workaround, not the upstream diagnostic fix.

shleder · 2 hours ago

Regarding config.toml permission block parsing errors and static config enforcement:

Managing fine-grained permissions inside complex TOML/JSON configs often introduces syntax drift and parser edge cases across CLI updates.

If you need deterministic, system-level security boundaries that don't depend on application config file parsing:

Vetto enforces immutable OS rules outside the agent runtime:

# Wrap Codex in a verified kernel sandbox:
npx @shledery/vetto --agent codex --profile strict -- codex

Key Differences:

  • Out-of-Tree Enforcement: Security rules are applied via Linux Landlock / macOS Seatbelt at process spawn time, completely independent of config.toml parsing.
  • Fail-Closed Policy Engine: If policies fail to validate, Vetto aborts with exit code 1 instead of silently falling back to unconfined execution.