Skip to content

fix(codex): ensure hcom DB is writable in sandbox mode#36

Open
khoant94 wants to merge 1 commit into
aannoo:mainfrom
khoant94:fix/codex-sandbox-writable
Open

fix(codex): ensure hcom DB is writable in sandbox mode#36
khoant94 wants to merge 1 commit into
aannoo:mainfrom
khoant94:fix/codex-sandbox-writable

Conversation

@khoant94

@khoant94 khoant94 commented May 2, 2026

Copy link
Copy Markdown
Contributor

Problem Statement

In Codex versions prior to 0.128.0, one-off CLI flags like --add-dir could be dropped or ignored during the reconciliation of configuration layers and permission profiles. Specifically, when a named profile or environment-level sandbox setting is resolved, it may supersede the filesystem permissions granted via the --add-dir launch flag without warning. This frequently resulted in "Operation not permitted" errors when hcom attempted to write to its SQLite database, even though the flag appeared correctly in the process list. This is a known persistence bug tracked in openai/codex#11401.

Solution

This PR implements a "defense-in-depth" approach to directory permissions. Instead of relying solely on the --add-dir flag, we now explicitly inject a direct configuration override:
-c sandbox_workspace_write.writable_roots=["/path/to/hcom"]

By providing both the CLI flag and the config-level override, we ensure the hcom directory survives the configuration reconciliation process across all session types.

Key Changes:

  • Robust Flag Detection: Updated ensure_hcom_writable to use the CodexArgSpec parser, ensuring we correctly detect existing -c and --add-dir values.
  • Safe Path Injection: Used serde_json to escape the hcom_dir path, preventing injection vulnerabilities or parsing errors if the path contains quotes or special characters.
  • Dual Injection: Modified the logic to inject both the flag and the config override if they are missing.

Verification

  • Added test_ensure_hcom_writable_avoids_duplicate_config
  • Added test_ensure_hcom_writable_adds_config_override
  • Confirmed all codex_preprocessing tests pass via cargo test codex_preprocessing.

Injects a configuration override for 'sandbox_workspace_write.writable_roots'
alongside '--add-dir'. This addresses an issue in Codex (tracked as a known
limitation in versions prior to 0.128.0) where one-off CLI flags like
'--add-dir' could be dropped or ignored during configuration layer
reconciliation, particularly when permission profiles are being resolved.

By providing both the CLI flag and the direct configuration override,
we ensure that the hcom directory remains writable across all session
states and profile reconciliations.

- Use robust CodexArgSpec parser for flag detection
- Inject both --add-dir and -c config override
- Escape hcom_dir using serde_json to handle special characters
- Add regression tests for config injection and duplicate avoidance
@aannoo

aannoo commented May 5, 2026

Copy link
Copy Markdown
Owner

Hey @khoant94 I can't verify. do you have more info?


AI review:

Review: aannoo/hcom PR #36fix(codex): ensure hcom DB is writable in sandbox mode

Subject

  • Repo: aannoo/hcom, PR fix(codex): ensure hcom DB is writable in sandbox mode #36, author khoant94.
  • Diff: src/tools/codex_preprocessing.rs::ensure_hcom_writable — in addition to prepending --add-dir <hcom_dir>, also injects -c sandbox_workspace_write.writable_roots=["<hcom_dir>"]. Idempotence via spec.get_flag_value("-c").as_list().contains(&config_val) (exact-string).
  • Cited upstream: openai/codex#11401.
  • Pinned codex tree for verification: ../codex HEAD 35aaa5d9fc.

Author's stated premise (verbatim summary)

  1. On Codex < 0.128.0, --add-dir could be dropped during reconciliation of config layers and permission profiles.
  2. A named profile or environment-level sandbox setting could supersede the FS permissions granted by --add-dir, producing Operation not permitted on hcom DB writes.
  3. Defense-in-depth: also inject -c sandbox_workspace_write.writable_roots=["<hcom_dir>"] so the directory survives reconciliation.

Verdict

Reject. The premise does not hold against the pinned codex tree, and the change introduces a silent regression on a real configuration. Detailed below.


Finding 1 — -c overrides REPLACE; PR introduces a silent regression

Codex semantics

codex-rs/utils/cli/src/config_override.rs:79-89 (doc):

"Apply all parsed overrides onto target. Intermediate objects will be created as necessary. Values located at the destination path will be replaced."

apply_single_override body (config_override.rs:101-121): terminal step is tbl.insert((*part).to_string(), value) — unconditional replace at the leaf key. Arrays are not array-merged; they are overwritten wholesale by virtue of being a non-Table leaf.

Concrete regression case

User config in ~/.codex/config.toml:

[sandbox_workspace_write]
writable_roots = ["/some/user/root"]

Launch via hcom codex ...:

  1. ensure_hcom_writable (post-PR) prepends -c sandbox_workspace_write.writable_roots=["<hcom_dir>"].
  2. The CLI override layer (source SessionFlags) replaces the merged-config leaf at sandbox_workspace_write.writable_roots with ["<hcom_dir>"].
  3. with_additional_legacy_workspace_writable_roots(&additional_writable_roots) then merges the --add-dir path additively into the runtime sandbox policy. Final effective roots: ["<hcom_dir>"].
  4. /some/user/root has been silently dropped purely as a side effect of launching through hcom.

Without the PR, --add-dir <hcom_dir> alone yields a final policy containing ["/some/user/root", "<hcom_dir>"] (preserved user intent). The PR strictly worsens this scenario.

PR test gap

test_ensure_hcom_writable_avoids_duplicate_config and test_ensure_hcom_writable_adds_config_override (PR diff in src/tools/codex_preprocessing.rs) assert presence/exact-string idempotence only. Neither covers coexistence with a user-supplied sandbox_workspace_write.writable_roots of any form. The regression is therefore invisible to the new tests.


Finding 2 — --add-dir is NOT dropped at launch on current codex (in hcom's default modes)

Scope: hcom has four sandbox modes (hcom/src/tools/codex_preprocessing.rs:18-56). workspace and untrusted emit --sandbox workspace-write; danger-full-access emits --dangerously-bypass-approvals-and-sandbox; none emits nothing and ensure_hcom_writable early-returns without injecting --add-dir. The Finding below applies to workspace/untrusted only — the modes where sandbox_mode_override.is_some(). In none, the user's profile/[permissions] resolution applies normally (see plausible-causes §5).

Trace, for the --sandbox workspace-write launch shape:

  • codex-rs/cli/src/main.rs:1372additional_writable_roots: shared.add_dir flows into ConfigOverrides.
  • core/src/config/mod.rs:1687-1695resolve_permission_config_syntax: when sandbox_mode_override.is_some(), returns Some(PermissionConfigSyntax::Legacy) unconditionally. Profile machinery does not engage for these hcom-launched sessions.
  • core/src/config/mod.rs:2214-2221profiles_are_active becomes false in this branch; using_implicit_builtin_profile becomes true under the legacy syntax conditions.
  • core/src/config/mod.rs:2311-2322 — for WorkspaceWrite, with_additional_legacy_workspace_writable_roots(&additional_writable_roots) is invoked when using_implicit_builtin_profile. --add-dir is merged additively.
  • core/src/config/mod.rs:2421-2422 — final legacy else branch (no profile, no profiles-active) likewise merges additional_writable_roots via with_additional_legacy_workspace_writable_roots.

Conclusion: under hcom's workspace/untrusted launch shape, --add-dir is merged into the sandbox policy on multiple covered branches. There is no reconciliation step on this tree that drops it.


Finding 3 — sandbox_workspace_write is the wrong knob for explicit profiles

core/src/config/mod.rs:2278-2282:

let builtin_workspace_write_settings = if using_implicit_builtin_profile {
    cfg.sandbox_workspace_write.as_ref()
} else {
    None
};

cfg.sandbox_workspace_write is consulted only when using_implicit_builtin_profile. Explicit profile selection (named or permission_config_syntax = Some(Profiles)) ignores [sandbox_workspace_write] entirely; in those branches additional roots flow via with_additional_writable_roots(cwd, &additional_writable_roots) (line 2318-2321) — i.e., still the --add-dir path, not the config override.

Codex's own test fixates this:

  • core/src/config/config_tests.rs:1431-1437default_permissions_read_only_applies_additional_writable_roots_as_modifications asserts: "explicit :workspace should not inherit sandbox_workspace_write roots as concrete grants…"
  • config_tests.rs:1498-1537implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_settings confirms preservation only for the implicit case.

So the PR's injected -c sandbox_workspace_write.writable_roots=... cannot defend the "named profile / explicit profile reconciliation" scenario named in the PR description, by construction. It is the wrong layer.


Finding 4 — openai/codex#11401 is mis-cited

The cited bug reproduces when the user, after launch, opens /permissions in the TUI and selects "Default", which re-derives the policy at runtime. The runtime re-derivation does not source the legacy [sandbox_workspace_write] table for explicit profile selection (Finding 3). A launch-time -c injection therefore cannot reach this bug. The PR's framing ("This frequently resulted in 'Operation not permitted' errors when hcom attempted to write to its SQLite database") is not the failure mode #11401 documents.

If a real EPERM on hcom DB writes was observed, the failure was either (a) on a Codex predating the current additive-path implementation (specific commit not provided), (b) in the codex + hcom start join flow which bypasses ensure_hcom_writable entirely (src/commands/start.rs:672), or (c) from a different cause. Without the actual command line, codex version, and config, the PR is speculative.


Finding 5 — exact-string idempotence is fragile

Idempotence check (PR diff):

let has_config = if let Some(val) = spec.get_flag_value("-c") {
    val.as_list().contains(&config_val.as_str())
} else { false };

config_val is the literal format!("sandbox_workspace_write.writable_roots=[{}]", quoted_hcom_dir). Any user-supplied -c sandbox_workspace_write.writable_roots=[...] with different formatting (whitespace, quoting style, additional paths, alternate dotted-path canonicalization) bypasses dedup. Result: two -c overrides at the same dotted path; codex applies both via apply_on_value in iteration order (config_override.rs:82-88), last-write wins. The hcom-injected one is prepended → user's later override wins → hcom dir effectively absent in cases where user set the leaf.

This interacts poorly with Finding 1: in the configured-but-unspecified-as-arg case, hcom replaces silently; in the configured-as-arg case, hcom is ineffective. There is no input shape where the PR is strictly safer than --add-dir alone on the current codex tree.


Architectural note (orthogonal to PR merge decision)

hcom defaults HCOM_DIR to ~/.hcom (src/paths.rs:26-53) and forwards it into the codex child via launcher env (src/launcher.rs). Codex's sandbox model is workspace-centric and additive; --add-dir is the canonical additive knob. The repo already documents the workspace-local mode HCOM_DIR="$PWD/.hcom" in src/commands/help.rs as the sandbox-friendly choice. Auto-switching is undesirable — it would partition the cross-project mesh that cross-host LUNA agents rely on. Out of scope for this PR; flag as discussion if the author wants a path forward.

hcom start join flow (vanilla codex started independently, then joins via hcom start) bypasses ensure_hcom_writable (src/commands/start.rs:672). No launch-time flag injection rescues that case. If the original EPERM was observed there, this PR is not the fix.


Plausible real causes of the reported EPERM

None is addressed by the PR; several are strictly worsened by it.

Failure-surface taxonomy (precondition)

The PR description treats "hcom DB write" as a single sandboxed surface. It is not. Three distinct paths, with different sandbox semantics:

  • Hooks (codex-side hooks.json handlers). codex-rs/hooks/src/engine/command_runner.rs:33-41 spawns hooks via plain tokio::process::Command with no bwrap/landlock/seatbelt wrapping. Hooks run as direct codex children with the same FS permissions codex itself has. Sandbox policy, profile reconciliation, [sandbox_workspace_write], and protected-metadata rules do not apply to hooks. A hook→DB EPERM cannot originate from codex sandbox config — it would have to be OS-level (mode bits, ownership, readonly mount).
  • Agent shell tool (hcom send etc. invoked by the model). Goes through codex's exec sandbox. On denial: core/src/exec.rs:762 returns SandboxErr::Denied, which routes to core/src/tools/runtimes/shell/unix_escalation.rs:466 request_command_approval. The user gets an interactive approval prompt; only AskForApproval::Never produces a hard fail without prompt.
  • apply_patch to protected metadata. core/src/safety.rs:169 consults FileSystemSandboxPolicy::can_write_path_with_cwd before exec; protected-metadata writes are policy-rejected with no escalation. This is the only path that produces a silent hard-deny on the cited names.

Categorize each hypothesis below by surface to know whether the symptom is "user gets a prompt" (annoying), "operation is silently denied" (broken), or "hook silently no-ops" (mesh disconnect).

Hypotheses

  1. Codex profile-derive window, ~2026-04-27 → 2026-04-30. Surface: agent shell. Between 755880ef9c ("permissions: derive config defaults as profiles", 04-27) and f3fa7a67a9 ("config: gate legacy writable roots from profiles", 04-30), the else if profiles_are_active branch in Config::new gated additional_writable_roots injection on a single if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }). If the user had a [permissions] table in ~/.codex/config.toml that derived a non-WorkspaceWrite profile, --add-dir was silently dropped at launch and the model's hcom send calls would be sandbox-denied (escalated to approval). Tag dates rust-v0.122.0-alpha.6 (04-17) and rust-v0.129.0-alpha.2 (05-01) bracket the window and match the PR's "<0.128.0" framing. Fix: upgrade codex. The -c sandbox_workspace_write.writable_roots injection does not help because explicit profiles ignore that table (Finding 3).
  2. Symlinked HCOM_DIR pre-2026-04-10. Surface: agent shell + apply_patch. Commit b114781495 fixed permission normalization that canonicalized symlinked writable roots away from the logical paths the sandboxed process used; cited surface was "shell and apply_patch failures on symlinked writable roots". Triggers: macOS /tmp/private/tmp, WSL bind mounts, Termux /storage/emulated/0 symlinks. Fix: upgrade codex.
  3. hcom start join flow. Surface: hooks (and indirectly agent shell). commands/start.rs:672 attaches hcom's hook config to a vanilla codex session that was launched without --add-dir. Hook→DB writes are unaffected (hooks bypass sandbox), so hcom mesh participation works. But the agent in that session, when calling hcom send, still hits codex's sandbox without the hcom dir whitelisted — surfaces as approval prompts (workspace-write) or hard fail (Never). PR fix(codex): ensure hcom DB is writable in sandbox mode #36 does not reach this path. Hcom-side options: emit a one-line note at hcom start time pointing at --add-dir, or rely on codex's prompt UX.
  4. HCOM_DIR nested under .git/, .codex/, .agents/ (post-2026-04-28). Surface: apply_patch only (per safety.rs:169); agent shell writes via direct exec are also denied by OS sandbox layer; hooks are unaffected. Commits 0156b1e61f/0670d8971a/74f06dcdfb policy-deny writes to those metadata names. Hcom-side fix: refuse such HCOM_DIR in paths::resolve_hcom_dir_from_env — the codex prompt flow does not rescue this case (it is pre-exec deny, not denial-after-exec).
  5. User [permissions] resolves non-WorkspaceWrite. Surface: agent shell. Engages only when hcom does not force sandbox_mode_override, i.e., mode=none (per Finding 2 scope). In workspace/untrusted modes this hypothesis does not apply. In mode=none, --add-dir is correctly ignored, and PR fix(codex): ensure hcom DB is writable in sandbox mode #36's injection is also ignored (Finding 3). User must adjust their profile or switch to workspace/untrusted.
  6. User passed --sandbox read-only to hcom codex. Surface: agent shell. codex_preprocessing.rs:60-62 already notes --add-dir is ignored under read-only. The -c injection is also ineffective (only WorkspaceWrite reads sandbox_workspace_write). On denial codex prompts; on Never it hard-fails. Hcom should warn at preprocess.
  7. Tool-home env desync in workspace-local mode. Surface: hooks (silent no-op, not EPERM). hcom forwards HCOM_DIR to the codex child (launcher.rs:870-910) but never sets CODEX_HOME. With HCOM_DIR=$PWD/.hcom (documented at help.rs:435), hcom installs hooks to parent(HCOM_DIR)/.codex/hooks.json while the codex child reads ~/.codex/hooks.json (its default) — hooks orphaned, agent never connects to mesh. Note: hcom/src/hooks/codex.rs:442-449 codex_config_dir() is a single resolver consulted by both reads and writes, so the bug is in env propagation, not in tool_config_root(). (Android 0d5c78103d makes this acute by defaulting CODEX_HOME=/data/local/tmp/codex.) Hcom-side fix: in workspace-local mode, set CODEX_HOME (and equivalents for claude/gemini) on the child to tool_config_root()/.codex, unless user already exported a value.
  8. Bubblewrap fatal pre-2026-03-17 if HCOM_DIR did not exist (603b6493a9). Surface: agent shell. Already fixed upstream.
  9. WSL/Windows quirks. #11401 was on WSL v0.98.0. Antivirus, case-folding, /mnt/ bind mounts produce real EPERM/EACCES unrelated to profile reconciliation. Treat WSL reports skeptically; require non-WSL repro.
  10. Codex 0.128 --full-auto removal (codex_preprocessing.rs:14-17). Custom user launchers that relied on --full-auto for workspace-write may have silently fallen through to read-only on upgrade — same symptom, unrelated cause.

Distinguishing diagnostics for the author

The repro recipe below (1–5) plus, additionally:

codex --version
realpath "${HCOM_DIR:-$HOME/.hcom}"; realpath "$HOME"
grep -E "^\[permissions|^default_permissions|^\[sandbox_workspace_write" ~/.codex/config.toml 2>/dev/null
case "${HCOM_DIR:-$HOME/.hcom}" in */.git/*|*/.codex/*|*/.agents/*) echo PROTECTED;; esac
uname -a; [ -f /proc/version ] && grep -i microsoft /proc/version

Reproduction recipe (for the author)

To distinguish a real bug from a speculative defense, request:

  1. Codex version (codex --version) and commit if buildable.
  2. Exact ps-visible launch command line that produced Operation not permitted.
  3. Contents of ~/.codex/config.toml and any CODEX_HOME-relative profile/environment files.
  4. Whether /permissions was used at runtime before the failure.
  5. Whether the failing session was launched via hcom codex … or codex … followed by hcom start.

Without (1)–(5), the change is not justifiable against the regression in Finding 1.


Recommended outcomes

  1. Close as-is. Premise unsupported on HEAD 35aaa5d9fc; introduces silent regression for users with custom sandbox_workspace_write.writable_roots; targets wrong layer for the explicit-profile case it claims to harden; does not reach the runtime /permissions reset described in #11401.
  2. If kept, the override must merge with the user's existing writable_roots (read merged config, compute union, write). A scalar -c override at an Array path is fundamentally unsuitable. Add tests covering: (a) pre-existing [sandbox_workspace_write] writable_roots in user config.toml; (b) pre-existing user-supplied -c sandbox_workspace_write.writable_roots=[...] arg; (c) explicit profile selection.
  3. Discussion issue, not a fix. File the architectural mismatch (global hcom control plane vs workspace-scoped codex sandbox) as a separate discussion. Do not couple to PR fix(codex): ensure hcom DB is writable in sandbox mode #36.

Pinned references

Claim File Lines
Override semantics: replace at leaf ../codex/codex-rs/utils/cli/src/config_override.rs 79-89, 101-121
Sandbox-mode override forces Legacy syntax ../codex/codex-rs/core/src/config/mod.rs 1687-1695
cfg.sandbox_workspace_write only used when implicit builtin ../codex/codex-rs/core/src/config/mod.rs 2278-2282
--add-dir merged additively (implicit builtin / WorkspaceWrite) ../codex/codex-rs/core/src/config/mod.rs 2311-2322
--add-dir merged additively (legacy fallback branch) ../codex/codex-rs/core/src/config/mod.rs 2421-2422
additional_writable_roots source ../codex/codex-rs/cli/src/main.rs 1372
Test: explicit :workspace does not inherit sandbox_workspace_write ../codex/codex-rs/core/src/config/config_tests.rs 1431-1437
Test: implicit builtin preserves sandbox_workspace_write.writable_roots ../codex/codex-rs/core/src/config/config_tests.rs 1498-1537
hcom ensure_hcom_writable (post-PR) src/tools/codex_preprocessing.rs 65-122 (PR diff)
hcom HCOM_DIR default src/paths.rs 26-59
hcom workspace-local mode docs src/commands/help.rs (search HCOM_DIR)
hcom join flow bypasses ensure_hcom_writable src/commands/start.rs ~672

@khoant94

khoant94 commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@aannoo it happened intermittently, there's no real pattern to reproduce yet. When it happened, codex can read from the db but failed to write.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants