Skip to content

Windows: writers under the config root can overlap the optional config-root ACL flight #7815

Open

Description

@lidge-jun

Client or integration

Other

Area

Platform (Windows / macOS / Linux)

Summary

On Windows, loadConfig() starts an optional, unawaited ACL hardening flight on the OpenCodex config directory (src/config/paths.ts, hardenConfigDir). Writers that create or harden files under that directory can run while that parent flight is still in progress. Parent inheritance work can then drop a freshly applied child grant, and the write fails or leaves the file with an unexpected ACL.

#7777 fixed this for the observed failure class: intercept CA, proxy token and picker secrets, and credential-bearing Claude settings now wait through withConfigDirHardeningSettled before their synchronous write. Other writers under the config root have the same overlap and are not yet gated. Known sites:

  • config.json saves (src/server/management/agent-settings-routes.ts around line 1590)
  • Claude Desktop 3p config-library publication (src/claude/desktop-3p.ts around lines 845 and 863)
  • the remote Desktop store hardening and publication (src/claude/desktop-remote-store-io.ts around lines 60 and 71)

Expected: every writer that applies a required ACL to a path under the config root waits for overlapping config-root ACL work, like the #7777 writers do, with the same bounded deadline and a clean refusal on timeout.

This predates v2.92.0. The optional flight dates from #3257 and was last changed in #7107. It was scoped out of #7777 and of the v2.93.0 release by the release captain.

Reproduction

Intermittent, not consistently reproducible by hand. The overlap is deterministic with the test seams used by tests/claude-integration/claude-intercept-acl-barrier.test.ts:

  1. Hold the async icacls runner (setAsyncIcaclsRunnerForTests) and start hardenConfigDir() so a parent flight is in progress.
  2. Trigger one of the writers above.
  3. Observe descendant ACL commands for that writer's path while the parent flight is still held.

On hosted Windows CI the same class produced an empty DACL and ca_unavailable for the intercept CA (lane=all run 40389603119, Windows shard 7/9) before #7777.

Version

2.93.0 (dev 42549ba)

Operating system

Windows (hosted windows-latest runners and Windows 11)

Provider and model

No response

Logs or error output

_No response_

Screenshots and supporting files

No response

Redacted configuration

_No response_

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. coderabbitai commented on Oct 27, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #7777 - fix(claude): drain parent ACL flights before intercept secret writes [open]
    #7781 - fix(claude): report a timed-out first-party rollback as incomplete [open]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. added
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    on Oct 27, 2026
  3. lidge-jun commented on Oct 28, 2026

    @lidge-jun
    OwnerAuthor

    Partially addressed on dev by #7826 (6bc511d1b5f74160cf07e7a90a5e28a53d8d59e8). The async management and CLI writer phases now admit through withConfigDirHardeningSettled: config saves and partial updates (config-routes.ts, agent-settings-routes.ts), the native Desktop toggle, ocx claude-desktop, and Desktop gateway cleanup. Each callback covers a complete synchronous phase and derives its state inside admission. A timeout refuses before any commit, or goes through the existing rollback and partial-commit reporting after one.

    Still open here:

    • synchronous writer APIs that cannot await (saveConfig, the Desktop store I/O) and their other callers;
    • the integrations catalog writers (Hermes, Aside, shared catalog refresh, and the Aside sync route). These need a consumer-visible refusal code and GUI handling for partial results before they can be gated;
    • other config-root writers outside these routes, such as management token files, service secrets, spend-ledger lease and link store.
  4. lidge-jun commented on Oct 28, 2026

    @lidge-jun
    OwnerAuthor

    Partially addressed on dev by #7869 (fdc6dd8). The local writers that already run behind an async boundary now admit through withConfigDirHardeningSettled with the #7826 contract:

    • ocx config set, unset and import
    • offline ocx effort
    • custom model add and remove
    • provider add, remove and default, including the GCP credential cleanup after a provider removal
    • the Advisor settings route

    Each covers a complete synchronous phase, and preliminary reads no longer publish recovery backups outside admission. Two related behavior changes came with it:

    • An invalid, non-object or error-bearing existing config.json is now refused (CLI usage error, exit 2; write_failed on the Advisor route) with its bytes unchanged, instead of being replaced by defaults.
    • The Advisor route adopts its committed subtree together with its live merge baseline, so a later unrelated save cannot restore a value that was removed on disk.

    Still open here:

    • Synchronous writer APIs that cannot await, and their other callers: saveConfig and persisted mutation (src/config.ts, src/config/persisted-mutation.ts), Desktop remote store I/O (src/claude/desktop-remote-store-io.ts), management and GUI session tokens (src/server/management-auth.ts, src/server/persistent-gui-session.ts), service secrets (src/service/guards.ts, src/lib/service-secrets.ts), the spend-ledger lease (src/lib/spend-ledger-owner.ts), and the link store and compensation (src/link/store.ts, src/link/compensation.ts). These need an async companion before they can be admitted.
    • The integrations catalog fan-out (src/integrations/catalog-refresh.ts) and the Aside sync route (src/server/management/aside-profile-routes.ts). These need a consumer-visible refusal code and GUI handling for partial results.
  5. lidge-jun commented on Oct 30, 2026

    @lidge-jun
    OwnerAuthor

    Partly addressed by #7998, merged into dev as 19ab113945530483f4524ae3e6f8374d16572976. Four timer-driven or async cache writers under the config root now admit their write through withConfigDirHardeningSettled, under the same deadline and refusal contract as #7777, #7826 and #7869:

    • the provider account quota cache
    • the Codex account quota cache
    • the reasoning support cache
    • the models.dev reasoning metadata refresh

    A timed-out admission skips the best-effort write, or fails the refresh unpublished. A cancel, clear or reset invalidates a write still waiting for admission.

    Still open, unchanged from the earlier breakdown: the synchronous writer APIs that cannot await (saveConfig, persisted mutation, Desktop remote store I/O, management and GUI session tokens, service secrets, the spend-ledger lease, and the link store and compensation), and the integrations catalog fan-out and Aside sync route.

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

    bugSomething isn't workingplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions