Skip to content

[Windows][Desktop][Automations] Scheduled heartbeat drops the selected named permission profile and sends legacy sandboxPolicy instead #50120

Open

Description

@karappo-kara

Summary

On Windows, a heartbeat automation targeting an existing local-project thread can replace the selected named permission profile with a generic approval mode.

My threads are configured to use the named permission profile youtube-pi-dev. After affected scheduled runs, the UI changes from that profile to the Japanese label 代わりに承認.

For one affected thread, the captured execution logs also show that:

  • active_permission_profile disappears.
  • The recorded filesystem permission definition changes.
  • An explicit write entry for the project directory is no longer present.
  • approval_policy and approvals_reviewer remain unchanged.

A read-only investigation of the installed application's bundled JavaScript identified a likely cause: the heartbeat request-construction path receives the saved named profile but does not forward its ID. Instead, it explicitly sends the legacy sandboxPolicy to turn/start.

No proposed fix has been applied to the installed application. The original reproduction and the additional observation on October 2 occurred without applying the proposed patch.

Environment

  • Operating system: Windows 11
  • Installed app package: OpenAI.Codex
  • Desktop app version inspected: 26.928.3736.0
  • Application code collected: October 2, 2026, approximately 01:21 JST
  • CLI version recorded in the affected thread's session_meta: 0.159.2
  • Local project directory: D:\program_works\python\youtube_pi_agent_gui
  • Named permission profile: youtube-pi-dev
  • Profile inheritance: :workspace
  • Approval policy in the compared records: on-request
  • Approvals reviewer in the compared records: auto_review
  • Automation type: heartbeat, targeting an existing thread
  • Schedule for the directly investigated automation: every 30 minutes

The 0.159.2 value above comes from recorded session metadata. It is not a separately verified identification of every runtime process active during the investigation.

The project configuration contains default_permissions = "youtube-pi-dev", and the project is marked as trusted. The named profile is defined in the configuration and is present in normal/manual turn records.

Expected behavior

A heartbeat automation targeting a thread should preserve the named permission profile selected for that thread, including its identity and intended permissions, subject to applicable managed restrictions.

For a thread using youtube-pi-dev, the scheduled request should preserve that selection through permissions: "youtube-pi-dev" or an equivalent supported mechanism.

It should not silently replace the named profile with a legacy sandbox-policy representation that does not retain the profile identity and may produce a different permission definition.

Actual behavior and captured evidence

Persisted selection

At the time of capture, the desktop's heartbeat-thread-permissions-by-id state still contained the named profile for the affected thread, even though intervening execution records lacked it.

The following is the captured entry with the Windows username and thread ID redacted:

{
  "activePermissionProfile": {
    "id": "youtube-pi-dev",
    "extends": ":workspace"
  },
  "approvalPolicy": "on-request",
  "approvalsReviewer": "auto_review",
  "sandboxPolicy": {
    "type": "workspaceWrite",
    "writableRoots": [
      "C:\\Users\\<USER>\\.codex\\visualizations\\2026\\10\\01\\<THREAD_ID>"
    ],
    "networkAccess": true,
    "excludeTmpdirEnvVar": false,
    "excludeSlashTmp": false
  }
}

This shows that the named selection was present in the captured desktop state. It does not establish that every in-memory state was identical at every execution boundary.

Execution-context comparison

The following two records come from the same affected thread on October 1, 2026. All times are JST, UTC+09:00.

Field 17:02:15 — named profile present 17:16:41 — named profile absent
active_permission_profile {"id":"youtube-pi-dev","extends":":workspace"} Field absent
cwd D:\program_works\python\youtube_pi_agent_gui Same
model gpt-6.1-sol Same
approval_policy on-request Same
approvals_reviewer auto_review Same
Explicit project-directory write entry in the detailed filesystem policy Present Absent
Legacy sandbox_policy.writable_roots Visualization directory only Same directory
exclude_tmpdir_env_var false true
exclude_slash_tmp false true

The legacy writable_roots list was the same, but the complete legacy policy was not identical because the temporary-directory flags also changed.

Representative records from that thread show the following pattern:

2026-10-01 17:02:15 JST — active_permission_profile = youtube-pi-dev
2026-10-01 17:16:41 JST — active_permission_profile field absent
2026-10-01 17:17:25 JST — active_permission_profile = youtube-pi-dev
2026-10-01 17:46:41 JST — active_permission_profile field absent
2026-10-01 18:16:51 JST — active_permission_profile field absent
2026-10-01 18:21:09 JST — active_permission_profile = youtube-pi-dev

The recurring approximately 30-minute pattern is consistent with the automation schedule. However, not every record in this timeline has been individually correlated with a captured live request, so the timeline alone should not be treated as proof of the trigger for every entry.

Scope of the observed impact

The logs establish a change in the recorded filesystem permission definition, not an operating-system access test.

In particular:

  • The loss of the explicit project write entry was recorded.
  • Actual OS-level write denial was not tested as part of this investigation.
  • The disappearance of an explicit read entry for the Python installation does not prove that Python became unreadable, because a broader root-read entry remained.
  • Network enablement and per-domain authorization were not treated as equivalent. Loss of all configured domain permissions has not been demonstrated.
  • No sandbox escape or privilege escalation has been demonstrated.

Reproduction pattern

This describes the pattern observed in my existing environment, not a verified clean-install minimal reproducer.

  1. Use a trusted local project with a valid named permission profile.
  2. Select that profile for an existing thread.
  3. Confirm that a normal/manual turn records the intended active_permission_profile.
  4. Configure a heartbeat automation targeting the same thread.
  5. Let the automation run on schedule.
  6. Compare the UI selection and the resulting turn_context with the preceding named-profile turn.

Multiple threads in the same project have exhibited the UI symptom. Detailed before/after execution-context evidence was collected for one affected thread.

The exact conditions distinguishing an affected run from a run that retains the profile are not yet established. I am not claiming that every scheduled execution fails.

Additional observation on October 2: the previously unaffected main thread also changed

Initially, one older “main” thread appeared to retain youtube-pi-dev, while other threads exhibited the problem.

On the morning of October 2, 2026, JST, I also observed the permission selection change in that main thread.

At that time:

  • The proposed application fix had not been applied.
  • No post-fix real-device validation had been performed.

This is therefore an additional observation in an unpatched environment, not a regression caused by the proposed patch.

The main thread should no longer be described as an unaffected exception.

For this additional main-thread observation, the exact scheduled run, outgoing request, and resulting turn_context have not yet been correlated. I am reporting the observed selection change, not claiming that its detailed filesystem policy was separately verified.

The older main thread's captured working directory was under the previous C: location, whereas other affected threads used the D: location. This difference has not been established as the cause or a reliable workaround. The morning observation also shows that the older thread is not reliably protected from the symptom.

Technical investigation

The installed application's app.asar was read without modifying the installed application.

The investigation used the complete collected JavaScript modules and traced the heartbeat request path through final JSON serialization. This was an AI-assisted analysis, with an isolated request-packaging test described below.

1. The heartbeat entry point drops the named selection

File:

.vite/build/main-DkWgQSQe.js

Function: Jl

The following is the complete function, formatted for readability while retaining the bundled identifiers:

async function Jl({
    statsig: e,
    automation: t,
    thread: n,
    appServerConnection: r,
    collaborationMode: i,
    permissions: a,
    waitForCompletion: o,
    runTrigger: s
}) {
    let c = await Yl({
            thread: n,
            appServerConnection: r
        }),
        l = a ?? await Zl({
            appServerConnection: r,
            cwd: c.cwd
        }),
        u = Ql(t);

    await iu({
        automation: t,
        appServerConnection: r,
        sessionId: c.sessionId,
        threadId: c.id,
        cwd: c.cwd,
        prompt: u,
        collaborationMode: i,
        approvalPolicy: l.approvalPolicy,
        approvalsReviewer: l.approvalsReviewer,
        sandboxPolicy: l.sandboxPolicy,
        model: null,
        effort: null,
        cyberAccessProgram: await Xl(r, c, e),
        waitForCompletion: o,
        runTrigger: s
    });
}

The incoming saved permissions object can contain activePermissionProfile, but Jl forwards only approvalPolicy, approvalsReviewer, and sandboxPolicy from that object.

It does not forward the profile ID.

It also does not forward runtimeWorkspaceRoots when that field is present.

2. The downstream heartbeat path does not restore the missing ID

In the same file:

  • iu constructs the turn-start request with sandboxPolicy, without a named permissions field.
  • au, used for the completion-waiting branch, forwards that request without restoring the named profile.

The traced connection-side path continues through:

.vite/build/main-DkWgQSQe.js
  Jl
    -> iu / au

.vite/build/bootstrap-DvSEn4qy.js
  TN.startTurn
    -> mM.startTurn
    -> TN.sendInternalRequest
    -> Rj.sendMessage
    -> JSON serialization / transport

The metadata and compatibility transformations on this path did not add the missing profile ID.

This is distinct from the ordinary message-submission path involving $ue.startTurn, eue, and Yle / prepare. The existence of settings inheritance on that ordinary path does not establish that the heartbeat request receives the same supplementation.

3. The bundle already contains a named-profile/legacy-policy conversion helper

File:

.vite/build/src-ghAWefM3.js

Function: qn, exported as pa

Complete function:

function qn(e) {
    return e.activePermissionProfile == null
        ? { sandboxPolicy: e.sandboxPolicy }
        : { permissions: e.activePermissionProfile.id };
}

This helper sends the profile ID when a named selection exists, and the legacy sandbox policy otherwise.

The investigated heartbeat Jl -> iu path does not perform this distinction.

4. Comparison with the public runtime source

The public rust-v0.159.2 tag was inspected because it matches the version string recorded in the affected thread's session metadata.

The relevant source behavior is:

  • TurnRequestProcessor::build_thread_settings_overrides rejects simultaneous permissions and sandboxPolicy.
  • When permissions is supplied, the named profile is resolved using the thread configuration and working directory.
  • SessionConfiguration::apply has a separate legacy-policy path when a sandbox_policy override is supplied without a permission_profile.
  • That path calls set_legacy_permission_profile, which installs a legacy permission-profile snapshot.

References:

The running native binary was not compared byte-for-byte with that public source version. This comparison supports the diagnosis but is not a complete verification of every runtime behavior on the affected machine.

Isolated request-construction tests

An isolated test harness used the extracted application functions, with thread-resume responses, transport, notifications, and related dependencies replaced by test doubles.

The investigation compared:

  • 32 cases using the original request-construction code.
  • 32 cases using a proposed correction.

All 64 cases passed their respective assertions. For the original-code cases, those assertions included confirming the missing profile ID; they do not mean that the original behavior was correct.

The combinations covered scheduled versus “run now” invocation, mocked compatibility branches, the presence or absence of runtime workspace roots, and named-profile versus legacy inputs.

Representative results:

Input Original outgoing request Proposed correction
Main-thread-equivalent saved state with youtube-pi-dev No named permissions; legacy sandboxPolicy present permissions: "youtube-pi-dev"; no sandboxPolicy
Affected-thread-equivalent saved state with youtube-pi-dev No named permissions; legacy sandboxPolicy present permissions: "youtube-pi-dev"; no sandboxPolicy
Built-in named selection :workspace No named permissions; legacy sandboxPolicy present permissions: ":workspace"; no sandboxPolicy
No named profile Legacy sandboxPolicy Legacy behavior retained

These tests validate request packaging only. They do not constitute a Windows application reproduction, a test of the native runtime, an OS sandbox test, or proof that the proposed fix resolves every execution condition.

Suggested fix direction

Preserve the selected named profile throughout heartbeat request construction.

When a valid saved activePermissionProfile exists, pass its ID using permissions and do not simultaneously send sandboxPolicy.

When no named profile exists, retain the appropriate legacy behavior.

Please also review:

  • The fallback path when saved thread permissions are unavailable.
  • Cold-resume and existing-thread state interactions.
  • Whether existing runtimeWorkspaceRoots need to be preserved without replacing an absent value with an empty list.
  • Scheduled and “run now” execution, as well as regression coverage for ordinary manual turns.

The correction should retain the existing approval policy, approvals reviewer, model settings, input, notification behavior, completion handling, and applicable restrictions.

The proposed local correction targets Jl and iu, but it has not been applied to the installed application and has not received real-device validation.

Diagnostic material and current status

Available diagnostic material includes:

  • The relevant original bundled JavaScript from app version 26.928.3736.0.
  • Captured persisted permission-state entries.
  • Before/after execution-context records and a permission timeline.
  • A proposed correction.
  • The isolated request-packaging test results.

Redacted supporting material can be provided through an appropriate support channel.

Current status:

  • Request-construction omission identified in the inspected application code.
  • Detailed permission-definition changes recorded for an affected thread.
  • Additional UI occurrence observed in the previously unaffected main thread.
  • No proposed fix applied to the installed application.
  • No post-fix real-device or scheduled-run validation completed.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    windows-osIssues related to Codex on Windows systems
    app-serverIssues involving app server protocol or interfaces
    sandboxIssues related to permissions or sandboxing
    on Oct 1, 2026
  2. karappo-kara commented on Oct 2, 2026

    @karappo-kara
    Author

    Update: temporary local workaround and limited live validation

    The issue also reproduced after upgrading to desktop app version 26.928.4866.0. I then tested a local correction, with the following stages kept separate:

    1. Code inspection: The updated heartbeat request path still received saved named-profile information but did not forward the selected profile ID, and supplied the legacy sandboxPolicy to turn/start.

    2. Correction candidate: The revised code forwards the selected profile ID through the existing named-permission path and omits the legacy sandbox policy when a named profile is selected. The legacy fallback remains available when no named profile is selected. Approval policy and approval reviewer were preserved.

    3. Local application: The correction was applied to a separate application copy. The original Microsoft Store installation and WindowsApps contents were left unchanged, and backups were retained. The modified copy no longer has a valid original Authenticode signature; signature verification and application integrity enforcement were not disabled.

    4. Live validation: A manually triggered scheduled run retained the selected profile, although that run was interrupted. A separate, naturally scheduled run completed. Its native execution record retained active_permission_profile and the recorded detailed permission settings, including the intended project write access and enabled network access. The temporary test schedule was restored to its original content and paused state.

    These tests confirm profile and recorded permission retention for the tested cases. The natural-run test used no tools, so it did not independently exercise filesystem writes or network connectivity. Other profiles and all scheduling paths have not been validated.

    A later browser operation encountered a sandbox setup refresh error. Its relationship to this local correction is unknown.

    This remains an experimental local workaround pending an official fix. The issue should remain open.

  3. aetherxeg-source commented on Oct 2, 2026

    @aetherxeg-source

    A deeper reason to preserve the named profile here is:

    effective policy snapshot != authorization-policy identity

    Two named profiles can resolve to the same filesystem/network policy at one instant and still represent different authorization contracts. If an automation flattens the selected profile into a legacy snapshot, later managed-policy or profile changes can no longer be resolved against the identity the user actually selected.

    I would carry both:

    permission_profile_id
    + profile/resolution generation or digest
    -> resolved effective policy
    -> execution record
    

    rather than treating the resolved sandbox fields as a substitute for the profile identity.

    A useful conformance test would create two profiles P1/P2 that initially resolve to identical effective permissions, schedule a run under P1, then change a managed rule that makes P1 and P2 diverge before execution. The scheduled run should resolve and record P1 at execution time (or explicitly reject a stale profile contract), never continue from an indistinguishable flattened snapshot.

    That makes the invariant explicit:

    same permissions now != same authorization semantics over time.

  4. karappo-kara commented on Oct 2, 2026

    @karappo-kara
    Author

    Thanks for the suggestion. The workaround already preserves the selected profile ID and passes it through the existing named-profile resolution path. The contents of the selected youtube-pi-dev profile itself were not changed as part of this workaround; the change was limited to preserving and forwarding the profile identity through the scheduled-run path. I have not tested policy changes between scheduling and execution yet. I see generation/digest tracking as a separate design consideration beyond this targeted forwarding fix.

  5. karappo-kara commented on Oct 4, 2026

    @karappo-kara
    Author

    ``

    Update: profile identity loss still reproduces after the Microsoft Store update

    I completely closed the desktop application, updated it through Microsoft Store, rebooted Windows, and launched the normal Store-installed application. This test did not use the separately modified application copy described in my previous comment.

    Application-reported version information:

    • Before this update: 26.928.40906, build 12694, release channel prod

    • After this update: 26.930.31730, build 12947, release channel prod

    On October 4, 2026, I compared a manual turn at 10:51:07 JST with one naturally scheduled heartbeat turn at 11:14:15 JST, in the same existing thread. The heartbeat input and application turn were correlated with the matching turn ID in the native execution record.

    Recorded setting | Manual turn | Naturally scheduled heartbeat -- | -- | -- Selected named profile | youtube-pi-dev, extending :workspace | Selected named profile not recorded Explicit write permission for the D: project directory | Present | Present Network setting | Enabled | Enabled approval_policy | on-request | on-request approvals_reviewer | auto_review | auto_review exclude_tmpdir_env_var / exclude_slash_tmp | false / false | true / true

    The named-profile retention failure still reproduced. However, unlike the earlier detailed reproduction, the explicit D: project write permission remained present in this run.

    The UI changed from youtube-pi-dev to 「承認を依頼」 (“Ask for approval”). The earlier report showed 「代わりに承認」 (“Approve for me”). Despite this difference in the displayed label, the recorded approval policy and reviewer remained unchanged. The complete filesystem permission definition was also not identical, so this cannot be described as only a cosmetic change.

    Separate manual checks of the in-app browser, PowerShell-to-Python environment-variable inheritance, ordinary temporary-file operations, and fresh Node startup succeeded. These are separate execution paths and do not establish that scheduled runs preserve the selected permission profile.

    This scheduled test inspected recorded permissions; it did not exercise project writes or network connectivity. I have not inspected the updated application's request-construction code, so I cannot yet determine whether the differences reflect a partial fix, different saved state, or another cause. I also have not established whether the generic UI label results from newly introduced initialization behavior.

    The temporary test schedule was restored to its original configuration and paused state. After recording the failure, I manually reselected youtube-pi-dev; that reselection is not evidence of a scheduled-run fix.

    The issue remains reproducible in the tested normal Store installation and should remain open.

  6. info618 commented on Oct 11, 2026

    @info618

    Additional macOS reproduction of this natural-heartbeat profile loss, matching the request-construction behavior reported here.

    Environment: macOS / Mac mini; ChatGPT Desktop 26.1007.30912, build 20156, prod. Bundled Codex version 0.162.0-alpha.17.2. The configured named profile is b2m-hub, extending :workspace, with on-request and auto_review. Both retained projects explicitly select this profile; no legacy sandbox_mode or sandbox_workspace_write is present in the inspected user/project configuration layers. The installed updater offers no newer release.

    On October 11, 2026, two existing local chats showed:

    Execution Named profile Reviewer Policy
    Background message, 21:50:48 / 21:50:51 UTC b2m-hub auto_review on-request
    Visual tester natural heartbeat, 21:52:35 UTC absent auto_review on-request
    Automator natural heartbeat, 21:58:05 UTC absent auto_review on-request

    Each natural run was correlated to its heartbeat input and native turn ID. No tools were executed in those natural runs. The tester's explicit retained-checkout root write grant disappeared, while network access remained enabled. Settings and timer files were byte-identical across the compared runs. These are recorded-permission results, not additional filesystem/network tests under the faulty timer contexts.

    Read-only inspection of the installed Desktop bundle found that Gl receives saved permissions but passes approval policy, reviewer and legacy sandboxPolicy to tu; tu does not forward the selected activePermissionProfile.id through named permissions. No application or persisted-state patch was applied.

    Please provide an official Desktop correction preserving the selected named profile on natural heartbeat requests, using the named permissions field without also supplying legacy sandboxPolicy, while retaining approval reviewer, workspace roots and existing chat/worktree/schedule identities. The configuration allowlist is not a demonstrated substitute for forwarding the missing request field. We need a supported fix, not a modified application copy or alternate executor.

    Private account/contact information, usernames, chat IDs and local paths are omitted from this public reproduction.

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

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesautomationsbugSomething isn't workingsandboxIssues related to permissions or sandboxingwindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions