/goal continuation can reuse stale permission context after Full Access/custom permissions

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

Summary

Codex Desktop can continue a /goal run with stale permission settings even when the visible/global configuration is Full Access with approval_policy = "never".

This appears to be a /goal-specific path: current thread/goal/set sends only goal fields, while normal turn/start and thread/settings/update carry the current permission context. If the goal RPC immediately wakes a continuation, that continuation can inherit stale live/persisted thread settings.

Observed case

  • User-reported session/thread id: 019ef286-ba82-7eb0-bcad-2a85f8798cda
  • Platform: Windows Codex Desktop
  • App version observed in local logs: 26.616.71553
  • Effective user config at the time of repair:
approval_policy = "never"
sandbox_mode = "danger-full-access"
default_permissions = ":danger-full-access"

[windows]
sandbox = "elevated"

Evidence from local state/log inspection

  • The affected thread/state had stale approval_mode = 'on-request' with a managed/restricted sandbox policy despite global Full Access config.
  • The global heartbeat file had stale workspaceWrite / on-request permission contexts for several goal-related entries.
  • Normalizing the persisted state back to approval_mode = 'never' and full-access/disabled sandbox restored the expected behavior locally.
  • As of public main at e476fc16ce810495cbfabe4f815d20947da0ba79, ThreadGoalSetParams still contains only thread_id, objective, status, and token_budget.
  • turn/start and thread/settings/update already carry/apply approvalPolicy, approvalsReviewer, sandboxPolicy, and permissions.

Root cause hypothesis

/goal uses thread/goal/set, then goal runtime effects can immediately start or resume autonomous continuation. Because thread/goal/set does not carry the current permission context, that continuation can run under stale live/persisted thread settings that predate the user's current Full Access/custom-permissions selection.

This matches the user-visible symptom: normal turns may work after permission changes, but /goal can reintroduce old approval prompts or restricted sandbox behavior.

Proposed fix direction

I prepared a local patch draft against main that does the following:

  • Adds optional fields to ThreadGoalSetParams: approval_policy, approvals_reviewer, sandbox_policy, and permissions.
  • Sends the current permission context from TUI /goal set/status calls.
  • Resolves named/custom permission profiles in the app server before applying goal changes.
  • Adds CodexThread::apply_thread_settings_overrides(...) and applies goal permission context before set_thread_goal(...).apply_runtime_effects(...) can wake continuation.
  • Updates generated TypeScript and JSON schema fixtures.

I could not open a PR directly from this environment because the GitHub connector reports push: false for openai/codex and no PR-creation tool is exposed. The local patch passed git diff --check; generated JSON fixtures parse with ConvertFrom-Json. I could not run cargo fmt, cargo check, or Rust tests because cargo, rustc, and rustfmt are not installed on PATH in this Windows Codex Desktop environment.

Related issues

  • openai/codex#22090 describes essentially the same /goal stale permission context failure and was closed, but current public main still appears to lack permission context on ThreadGoalSetParams.
  • openai/codex#24326 was closed as a duplicate with similar full-access goal continuation behavior.
  • openai/codex#28574, openai/codex#28776, and openai/codex#28988 also look related to permission persistence/full-access prompt regressions.

Could someone on the Codex team confirm whether the intended fix is to propagate/apply permission context through thread/goal/set, or whether this should instead reuse the existing thread/settings/update path before goal runtime effects are applied?

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 2 months ago

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

  • #29610
  • #28296
  • #28574

Powered by Codex Action

rudy774 · 2 months ago

Patch draft generated locally against main (e476fc16ce810495cbfabe4f815d20947da0ba79). Static validation only: git diff --check passed and edited JSON fixtures parse with ConvertFrom-Json; Rust checks could not be run because cargo/rustc/rustfmt are unavailable in this environment.

<details>
<summary>Diff</summary>

diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
index 28597ef..791168a 100644
--- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json
+++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
@@ -3680,12 +3680,48 @@
     },
     "ThreadGoalSetParams": {
       "properties": {
+        "approvalPolicy": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/AskForApproval"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "approvalsReviewer": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/ApprovalsReviewer"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
         "objective": {
           "type": [
             "string",
             "null"
           ]
         },
+        "permissions": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "sandboxPolicy": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/SandboxPolicy"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
         "status": {
           "anyOf": [
             {
@@ -6834,4 +6870,4 @@
     }
   ],
   "title": "ClientRequest"
-}
\ No newline at end of file
+}
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
index 82b874b..653a6e8 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
@@ -17345,12 +17345,48 @@
       "ThreadGoalSetParams": {
         "$schema": "http://json-schema.org/draft-07/schema#",
         "properties": {
+          "approvalPolicy": {
+            "anyOf": [
+              {
+                "$ref": "#/definitions/v2/AskForApproval"
+              },
+              {
+                "type": "null"
+              }
+            ]
+          },
+          "approvalsReviewer": {
+            "anyOf": [
+              {
+                "$ref": "#/definitions/v2/ApprovalsReviewer"
+              },
+              {
+                "type": "null"
+              }
+            ]
+          },
           "objective": {
             "type": [
               "string",
               "null"
             ]
           },
+          "permissions": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "sandboxPolicy": {
+            "anyOf": [
+              {
+                "$ref": "#/definitions/v2/SandboxPolicy"
+              },
+              {
+                "type": "null"
+              }
+            ]
+          },
           "status": {
             "anyOf": [
               {
@@ -20686,4 +20722,4 @@
   },
   "title": "CodexAppServerProtocol",
   "type": "object"
-}
\ No newline at end of file
+}
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
index 81e427e..46aa316 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
@@ -15124,12 +15124,48 @@
     "ThreadGoalSetParams": {
       "$schema": "http://json-schema.org/draft-07/schema#",
       "properties": {
+        "approvalPolicy": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/AskForApproval"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "approvalsReviewer": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/ApprovalsReviewer"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
         "objective": {
           "type": [
             "string",
             "null"
           ]
         },
+        "permissions": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "sandboxPolicy": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/SandboxPolicy"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
         "status": {
           "anyOf": [
             {
@@ -18464,4 +18500,4 @@
   },
   "title": "CodexAppServerProtocolV2",
   "type": "object"
-}
\ No newline at end of file
+}
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalSetParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalSetParams.json
index 0087827..f82a3c3 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalSetParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalSetParams.json
@@ -1,6 +1,175 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
   "definitions": {
+    "AbsolutePathBuf": {
+      "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
+      "type": "string"
+    },
+    "ApprovalsReviewer": {
+      "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
+      "enum": [
+        "user",
+        "auto_review",
+        "guardian_subagent"
+      ],
+      "type": "string"
+    },
+    "AskForApproval": {
+      "oneOf": [
+        {
+          "enum": [
+            "untrusted",
+            "on-failure",
+            "on-request",
+            "never"
+          ],
+          "type": "string"
+        },
+        {
+          "additionalProperties": false,
+          "properties": {
+            "granular": {
+              "properties": {
+                "mcp_elicitations": {
+                  "type": "boolean"
+                },
+                "request_permissions": {
+                  "default": false,
+                  "type": "boolean"
+                },
+                "rules": {
+                  "type": "boolean"
+                },
+                "sandbox_approval": {
+                  "type": "boolean"
+                },
+                "skill_approval": {
+                  "default": false,
+                  "type": "boolean"
+                }
+              },
+              "required": [
+                "mcp_elicitations",
+                "rules",
+                "sandbox_approval"
+              ],
+              "type": "object"
+            }
+          },
+          "required": [
+            "granular"
+          ],
+          "title": "GranularAskForApproval",
+          "type": "object"
+        }
+      ]
+    },
+    "NetworkAccess": {
+      "enum": [
+        "restricted",
+        "enabled"
+      ],
+      "type": "string"
+    },
+    "SandboxPolicy": {
+      "oneOf": [
+        {
+          "properties": {
+            "type": {
+              "enum": [
+                "dangerFullAccess"
+              ],
+              "title": "DangerFullAccessSandboxPolicyType",
+              "type": "string"
+            }
+          },
+          "required": [
+            "type"
+          ],
+          "title": "DangerFullAccessSandboxPolicy",
+          "type": "object"
+        },
+        {
+          "properties": {
+            "networkAccess": {
+              "default": false,
+              "type": "boolean"
+            },
+            "type": {
+              "enum": [
+                "readOnly"
+              ],
+              "title": "ReadOnlySandboxPolicyType",
+              "type": "string"
+            }
+          },
+          "required": [
+            "type"
+          ],
+          "title": "ReadOnlySandboxPolicy",
+          "type": "object"
+        },
+        {
+          "properties": {
+            "networkAccess": {
+              "allOf": [
+                {
+                  "$ref": "#/definitions/NetworkAccess"
+                }
+              ],
+              "default": "restricted"
+            },
+            "type": {
+              "enum": [
+                "externalSandbox"
+              ],
+              "title": "ExternalSandboxSandboxPolicyType",
+              "type": "string"
+            }
+          },
+          "required": [
+            "type"
+          ],
+          "title": "ExternalSandboxSandboxPolicy",
+          "type": "object"
+        },
+        {
+          "properties": {
+            "excludeSlashTmp": {
+              "default": false,
+              "type": "boolean"
+            },
+            "excludeTmpdirEnvVar": {
+              "default": false,
+              "type": "boolean"
+            },
+            "networkAccess": {
+              "default": false,
+              "type": "boolean"
+            },
+            "type": {
+              "enum": [
+                "workspaceWrite"
+              ],
+              "title": "WorkspaceWriteSandboxPolicyType",
+              "type": "string"
+            },
+            "writableRoots": {
+              "default": [],
+              "items": {
+                "$ref": "#/definitions/AbsolutePathBuf"
+              },
+              "type": "array"
+            }
+          },
+          "required": [
+            "type"
+          ],
+          "title": "WorkspaceWriteSandboxPolicy",
+          "type": "object"
+        }
+      ]
+    },
     "ThreadGoalStatus": {
       "enum": [
         "active",
@@ -14,12 +183,48 @@
     }
   },
   "properties": {
+    "approvalPolicy": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/AskForApproval"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
+    "approvalsReviewer": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/ApprovalsReviewer"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
     "objective": {
       "type": [
         "string",
         "null"
       ]
     },
+    "permissions": {
+      "type": [
+        "string",
+        "null"
+      ]
+    },
+    "sandboxPolicy": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/SandboxPolicy"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
     "status": {
       "anyOf": [
         {
@@ -46,4 +251,4 @@
   ],
   "title": "ThreadGoalSetParams",
   "type": "object"
-}
\ No newline at end of file
+}
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalSetParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalSetParams.ts
index b92720c..1063d0f 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalSetParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalSetParams.ts
@@ -1,6 +1,9 @@
 // GENERATED CODE! DO NOT MODIFY BY HAND!
 
 // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { ApprovalsReviewer } from "./ApprovalsReviewer";
+import type { AskForApproval } from "./AskForApproval";
+import type { SandboxPolicy } from "./SandboxPolicy";
 import type { ThreadGoalStatus } from "./ThreadGoalStatus";
 
-export type ThreadGoalSetParams = { threadId: string, objective?: string | null, status?: ThreadGoalStatus | null, tokenBudget?: number | null, };
+export type ThreadGoalSetParams = { threadId: string, objective?: string | null, status?: ThreadGoalStatus | null, approvalPolicy?: AskForApproval | null, approvalsReviewer?: ApprovalsReviewer | null, sandboxPolicy?: SandboxPolicy | null, permissions?: string | null, tokenBudget?: number | null, };
diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs
index ba63f59..2ed1818 100644
--- a/codex-rs/app-server-protocol/src/protocol/common.rs
+++ b/codex-rs/app-server-protocol/src/protocol/common.rs
@@ -1968,6 +1968,10 @@ mod tests {
                 thread_id: "goal-thread".to_string(),
                 objective: Some("ship it".to_string()),
                 status: None,
+                approval_policy: None,
+                approvals_reviewer: None,
+                sandbox_policy: None,
+                permissions: None,
                 token_budget: None,
             },
         };
@@ -3532,6 +3536,10 @@ mod tests {
                 thread_id: "thr_123".to_string(),
                 objective: Some("ship goal mode".to_string()),
                 status: Some(v2::ThreadGoalStatus::Active),
+                approval_policy: None,
+                approvals_reviewer: None,
+                sandbox_policy: None,
+                permissions: None,
                 token_budget: Some(Some(10_000)),
             },
         };
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs
index 6a51f71..941243a 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs
@@ -769,6 +769,21 @@ pub struct ThreadGoalSetParams {
     pub objective: Option<String>,
     #[ts(optional = nullable)]
     pub status: Option<ThreadGoalStatus>,
+    /// Override the approval policy before a goal continuation can start.
+    #[experimental(nested)]
+    #[ts(optional = nullable)]
+    pub approval_policy: Option<AskForApproval>,
+    /// Override where approval requests are routed before a goal continuation can start.
+    #[ts(optional = nullable)]
+    pub approvals_reviewer: Option<ApprovalsReviewer>,
+    /// Override the sandbox policy before a goal continuation can start.
+    #[ts(optional = nullable)]
+    pub sandbox_policy: Option<SandboxPolicy>,
+    /// Select a named permissions profile id before a goal continuation can
+    /// start. Cannot be combined with `sandboxPolicy`.
+    #[experimental("thread/goal/set.permissions")]
+    #[ts(optional = nullable)]
+    pub permissions: Option<String>,
     #[serde(
         default,
         deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs
index 33cf142..0f1a63d 100644
--- a/codex-rs/app-server/src/message_processor.rs
+++ b/codex-rs/app-server/src/message_processor.rs
@@ -474,7 +474,9 @@ impl MessageProcessor {
         let thread_goal_processor = ThreadGoalRequestProcessor::new(
             Arc::clone(&thread_manager),
             outgoing.clone(),
+            arg0_paths.clone(),
             Arc::clone(&config),
+            config_manager.clone(),
             thread_state_manager.clone(),
             state_db.clone(),
             Arc::clone(&goal_service),
diff --git a/codex-rs/app-server/src/request_processors/thread_goal_processor.rs b/codex-rs/app-server/src/request_processors/thread_goal_processor.rs
index 5878047..92da2eb 100644
--- a/codex-rs/app-server/src/request_processors/thread_goal_processor.rs
+++ b/codex-rs/app-server/src/request_processors/thread_goal_processor.rs
@@ -9,7 +9,9 @@ use codex_goal_extension::GoalTokenBudgetUpdate;
 pub(crate) struct ThreadGoalRequestProcessor {
     thread_manager: Arc<ThreadManager>,
     outgoing: Arc<OutgoingMessageSender>,
+    arg0_paths: Arg0DispatchPaths,
     config: Arc<Config>,
+    config_manager: ConfigManager,
     thread_state_manager: ThreadStateManager,
     state_db: Option<StateDbHandle>,
     goal_service: Arc<GoalService>,
@@ -19,7 +21,9 @@ impl ThreadGoalRequestProcessor {
     pub(crate) fn new(
         thread_manager: Arc<ThreadManager>,
         outgoing: Arc<OutgoingMessageSender>,
+        arg0_paths: Arg0DispatchPaths,
         config: Arc<Config>,
+        config_manager: ConfigManager,
         thread_state_manager: ThreadStateManager,
         state_db: Option<StateDbHandle>,
         goal_service: Arc<GoalService>,
@@ -27,7 +31,9 @@ impl ThreadGoalRequestProcessor {
         Self {
             thread_manager,
             outgoing,
+            arg0_paths,
             config,
+            config_manager,
             thread_state_manager,
             state_db,
             goal_service,
@@ -115,6 +121,12 @@ impl ThreadGoalRequestProcessor {
         };
         let status = params.status.map(ThreadGoalStatus::to_core);
         let objective = params.objective.as_deref();
+        let token_budget = params.token_budget;
+
+        if let Ok(thread) = self.thread_manager.get_thread(thread_id).await {
+            self.apply_goal_permission_context(thread.as_ref(), &params)
+                .await?;
+        }
 
         let outcome = self
             .goal_service
@@ -126,7 +138,7 @@ impl ThreadGoalRequestProcessor {
                         .map(GoalObjectiveUpdate::Set)
                         .unwrap_or(GoalObjectiveUpdate::Keep),
                     status,
-                    token_budget: match params.token_budget {
+                    token_budget: match token_budget {
                         Some(token_budget) => GoalTokenBudgetUpdate::Set(token_budget),
                         None => GoalTokenBudgetUpdate::Keep,
                     },
@@ -162,6 +174,81 @@ impl ThreadGoalRequestProcessor {
         Ok(())
     }
 
+    async fn apply_goal_permission_context(
+        &self,
+        thread: &CodexThread,
+        params: &ThreadGoalSetParams,
+    ) -> Result<(), JSONRPCErrorError> {
+        if params.sandbox_policy.is_some() && params.permissions.is_some() {
+            return Err(invalid_request(
+                "`permissions` cannot be combined with `sandboxPolicy`",
+            ));
+        }
+
+        let has_permission_context = params.approval_policy.is_some()
+            || params.approvals_reviewer.is_some()
+            || params.sandbox_policy.is_some()
+            || params.permissions.is_some();
+        if !has_permission_context {
+            return Ok(());
+        }
+
+        let approval_policy = params
+            .approval_policy
+            .map(codex_app_server_protocol::AskForApproval::to_core);
+        let approvals_reviewer = params
+            .approvals_reviewer
+            .map(codex_app_server_protocol::ApprovalsReviewer::to_core);
+        let sandbox_policy = params.sandbox_policy.clone().map(|policy| policy.to_core());
+        let (permission_profile, active_permission_profile, profile_workspace_roots) =
+            if let Some(permissions) = params.permissions.clone() {
+                let snapshot = thread.config_snapshot().await;
+                let overrides = ConfigOverrides {
+                    workspace_roots: Some(snapshot.workspace_roots.clone()),
+                    default_permissions: Some(permissions),
+                    codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
+                    main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
+                    ..Default::default()
+                };
+                let config = self
+                    .config_manager
+                    .load_for_cwd(
+                        /*request_overrides*/ None,
+                        overrides,
+                        Some(snapshot.cwd().to_path_buf()),
+                    )
+                    .await
+                    .map_err(|err| config_load_error(&err))?;
+                if let Some(warning) = config.startup_warnings.iter().find(|warning| {
+                    warning.contains("Configured value for `permission_profile` is disallowed")
+                }) {
+                    return Err(invalid_request(format!(
+                        "invalid goal continuation settings: {warning}"
+                    )));
+                }
+                (
+                    Some(config.permissions.permission_profile().clone()),
+                    config.permissions.active_permission_profile(),
+                    Some(config.permissions.profile_workspace_roots().to_vec()),
+                )
+            } else {
+                (None, None, None)
+            };
+
+        thread
+            .apply_thread_settings_overrides(CodexThreadSettingsOverrides {
+                approval_policy,
+                approvals_reviewer,
+                sandbox_policy,
+                permission_profile,
+                active_permission_profile,
+                profile_workspace_roots,
+                ..Default::default()
+            })
+            .await
+            .map_err(|err| invalid_request(format!("invalid goal continuation settings: {err}")))
+    }
+
     async fn thread_goal_get_inner(
         &self,
         params: ThreadGoalGetParams,
diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs
index a522cd6..57a8c0f 100644
--- a/codex-rs/core/src/codex_thread.rs
+++ b/codex-rs/core/src/codex_thread.rs
@@ -355,6 +355,15 @@ impl CodexThread {
         self.codex.session.preview_settings(&updates).await
     }
 
+    /// Apply persistent thread settings overrides immediately.
+    pub async fn apply_thread_settings_overrides(
+        &self,
+        overrides: CodexThreadSettingsOverrides,
+    ) -> ConstraintResult<()> {
+        let updates = self.thread_settings_update(overrides).await;
+        self.codex.session.update_settings(updates).await
+    }
+
     async fn thread_settings_update(
         &self,
         overrides: CodexThreadSettingsOverrides,
diff --git a/codex-rs/tui/src/app/thread_goal_actions.rs b/codex-rs/tui/src/app/thread_goal_actions.rs
index a6f1ed5..34686e7 100644
--- a/codex-rs/tui/src/app/thread_goal_actions.rs
+++ b/codex-rs/tui/src/app/thread_goal_actions.rs
@@ -2,6 +2,7 @@ use super::App;
 use crate::app_event::AppEvent;
 use crate::app_event::ThreadGoalSetMode;
 use crate::app_server_session::AppServerSession;
+use crate::app_server_session::TurnPermissionsOverride;
 use crate::bottom_pane::SelectionAction;
 use crate::bottom_pane::SelectionItem;
 use crate::bottom_pane::SelectionViewParams;
@@ -199,8 +200,18 @@ impl App {
             } => (status, Some(token_budget)),
         };
 
+        let permissions_override = self.goal_permissions_override();
         let result = app_server
-            .thread_goal_set(thread_id, Some(objective), Some(status), token_budget)
+            .thread_goal_set(
+                thread_id,
+                Some(objective),
+                Some(status),
+                self.config.permissions.approval_policy.value().into(),
+                self.config.approvals_reviewer,
+                permissions_override,
+                self.config.cwd.as_path(),
+                token_budget,
+            )
             .await;
 
         match result {
@@ -237,6 +248,10 @@ impl App {
                 thread_id,
                 /*objective*/ None,
                 Some(status),
+                self.config.permissions.approval_policy.value().into(),
+                self.config.approvals_reviewer,
+                self.goal_permissions_override(),
+                self.config.cwd.as_path(),
                 /*token_budget*/ None,
             )
             .await;
@@ -255,6 +270,16 @@ impl App {
         }
     }
 
+    fn goal_permissions_override(&self) -> TurnPermissionsOverride {
+        if let Some(active_permission_profile) = self.config.permissions.active_permission_profile() {
+            return TurnPermissionsOverride::ActiveProfile(active_permission_profile);
+        }
+
+        TurnPermissionsOverride::LegacySandbox(
+            self.config.permissions.effective_permission_profile(),
+        )
+    }
+
     pub(super) async fn clear_thread_goal(
         &mut self,
         app_server: &mut AppServerSession,
diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs
index 527cde5..071ec59 100644
--- a/codex-rs/tui/src/app_server_session.rs
+++ b/codex-rs/tui/src/app_server_session.rs
@@ -918,9 +918,14 @@ impl AppServerSession {
         thread_id: ThreadId,
         objective: Option<String>,
         status: Option<ThreadGoalStatus>,
+        approval_policy: AskForApproval,
+        approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
+        permissions_override: TurnPermissionsOverride,
+        cwd: &std::path::Path,
         token_budget: Option<Option<i64>>,
     ) -> Result<ThreadGoalSetResponse> {
         let request_id = self.next_request_id();
+        let (sandbox_policy, permissions) = turn_permissions_overrides(permissions_override, cwd);
         self.client
             .request_typed(ClientRequest::ThreadGoalSet {
                 request_id,
@@ -928,6 +933,10 @@ impl AppServerSession {
                     thread_id: thread_id.to_string(),
                     objective,
                     status,
+                    approval_policy: Some(approval_policy),
+                    approvals_reviewer: Some(approvals_reviewer.into()),
+                    sandbox_policy,
+                    permissions,
                     token_budget,
                 },
             })

</details>

rpelevin · 2 months ago

I would make the goal path preserve the same invariant as a normal turn start: no continuation may wake under a permission context that is older than the user's current visible thread policy.

The dangerous ordering is not just schema drift. It is goal state update, then runtime effects, then autonomous continuation before approvalPolicy, approvalsReviewer, sandboxPolicy, and permission profile have been reconciled. I would test that at the boundary where thread/goal/set applies runtime effects.

Useful regression shape:

  1. Start a thread with a restrictive per-thread policy, then switch the visible policy to full access / never.
  2. Set or resume a goal, and assert the first continuation observes the current policy before any tool approval decision is made.
  3. Repeat with a named permissions profile, and fail closed if both a profile and explicit sandbox policy are supplied.
  4. After restart or state reload, stale persisted goal/thread rows cannot override the current policy without being visible as the active policy.

That keeps the product rule simple: whatever policy can authorize or block the next autonomous step must be the policy the user can inspect before that step starts.

Boundary: architecture and regression-test feedback only; no claim about using this project or running its code.

BGMLAI · 1 month ago

A continuation should snapshot the effective permission context at the moment it is scheduled, not reuse an earlier live context implicitly. The goal update and wake-up should be one atomic transition carrying a permission_context_version. If the visible profile changes before dispatch, the continuation should either rebind to the new context or stop with a clear mismatch, never merge fields from both. A regression can create a goal under restricted access, switch to full access, resume, and assert one coherent version across every continued turn.

sunjiajunsunjiajun · 1 month ago

I am seeing a similar stale-permission-context problem, but through subagent dispatch rather than only /goal continuation.

The parent agent is granted Full Access and reports that permissions were updated. However, when it dispatches a subagent, the child does not appear to receive the updated effective permission context. The child then behaves as if it is still running under a previous or more restricted sandbox/approval setting.

From the user perspective this is very hard to reason about: the main session says permissions were updated, but autonomous follow-on work can still run with stale permissions. This seems like the same underlying class of issue: continuation/delegated execution needs to receive the current effective permission context before it starts.

ashjo42 · 1 month ago

Fresh reproduction on 2026-07-26; this is still present in a newer Windows Desktop build.

Environment

  • Codex Desktop: 26.721.4979.0
  • OS: Windows 11 Pro 10.0.26200 (build 26200)
  • Visible user setting: Full Access
  • Intended approval policy: never
  • Workflow: existing persistent Goal controlling Unity through MCP

Observed behavior

After the Goal resumed/continued autonomously, the effective turn context was managed/restricted even though the user had selected Full Access. Ordinary Unity MCP/tool calls repeatedly surfaced approval UI, making unattended Goal execution unusable.

Important details:

  • The agent did not request escalation; the calls did not use require_escalated.
  • The affected Goal continuation reported a restricted/managed filesystem permission profile.
  • After returning to an interactive turn with Full Access reapplied, the effective context changed to danger-full-access, approval policy never, and unrestricted/disabled filesystem permissions.
  • The same Unity project and MCP connection were usable; the failure was the Goal continuation's permission context, not Unity.
  • The repeated prompts stopped the user from leaving a long-running visual/runtime acceptance workflow unattended.

Minimal reproduction shape

  1. Start or resume an existing Goal in Codex Desktop with Full Access.
  2. Let the Goal enter an autonomous continuation turn.
  3. Have the continuation invoke an MCP tool repeatedly (Unity MCP in this reproduction).
  4. Observe that each ordinary tool step can require user approval despite Full Access.
  5. Return to a foreground interactive turn/reapply Full Access; the effective permission context becomes unrestricted again.

Expected behavior

The first and every subsequent autonomous Goal continuation should inherit the current visible Full Access policy before any tool decision is made. A Goal must not silently fall back to a managed/restricted profile.

This makes Goal mode effectively unusable for unattended work on the current Windows Desktop build, so the issue is not limited to the older version originally reported.