0.149.0 rejects symlinked custom-agent role files with misleading "agent type is currently not available"

Open 💬 1 comment Opened Aug 22, 2026 by ezoterik

What version of Codex is running?

  • Failing CLI/app-server: codex-cli 0.149.0
  • Working control: codex-cli 0.148.0

What platform and surface are affected?

  • Remote Linux development host
  • Interactive CLI and an unmanaged remote app-server
  • The Desktop app itself was not downgraded
  • ChatGPT-authenticated Codex session
  • The installed custom-agent configuration was unchanged between the two tests

Summary

Codex 0.149.0 rejects configured custom-agent role files installed as symbolic links and returns only:

agent type is currently not available

The same symlink-based installation works after downgrading only the remote CLI/app-server to 0.148.0 and restarting it.

The affected installation intentionally links the individual role files from a version-controlled configuration repository into ~/.codex/agents/. All affected roles are symlinks, including implementer, code-mapper, and the other custom profiles.

Example sanitized layout:

~/.codex/agents/implementer.toml -> <configuration-repository>/codex-home/agents/implementer.toml
~/.codex/agents/code-mapper.toml -> <configuration-repository>/codex-home/agents/code-mapper.toml

No role contents, account secrets, or private repository paths are included here.

Likely root cause

This now appears directly related to #39299, merged into the 0.149.0 release:

That change explicitly says it will "reject symlinked user role files". It also changed custom-role reading from ordinary tokio::fs::read_to_string() to read_sensitive_file_to_string() and added the test:

apply_role_rejects_symlinked_role_file

The test expects the public error:

agent type is currently not available

This matches the observed 0.149.0 behavior exactly. The symlink restriction may be an intentional security boundary, but it creates an undocumented compatibility break for installations that previously managed custom-agent files through symlinks. The generic error also hides the actionable cause.

A second, distinct 0.149.0 custom-role regression associated with the bounded-role work is tracked in #40042:

#40042 concerns model_instructions_file being ignored and is not a duplicate of this report, but both appear related to the role-loading changes introduced by #39299.

Steps to reproduce

  1. Create a valid custom-agent role file outside ~/.codex/agents/.
  2. Symlink it into the normal custom-agent location:

``sh
ln -s /absolute/path/to/implementer.toml ~/.codex/agents/implementer.toml
``

  1. Register/use that role through the normal Codex custom-agent configuration.
  2. Start a fresh Codex CLI or app-server-backed task on 0.149.0.
  3. Attempt to spawn it:

``text
agent_type = "implementer"
fork_turns = "none"
``

  1. Observe:

``text
agent type is currently not available
``

  1. Substitute Codex CLI/app-server 0.148.0 without changing the role symlink or configuration.
  2. Fully restart the app-server and start a fresh task.
  3. Repeat the same read-only spawn probe.

Expected behavior

At minimum, Codex should return an actionable error explaining that symlinked custom-agent role files are not permitted.

Preferably, one of these contracts should be explicit:

  1. Safely allow role symlinks whose resolved targets satisfy the intended trust boundary; or
  2. Document that custom-agent role files must be physical files and reject symlinks during configuration loading with a clear path-safe warning.

The current behavior makes a discovered/configured role look generically unavailable and gives no indication that copying the same TOML into place may resolve it.

Actual behavior

In 0.149.0, the configured role is rejected before the child starts:

agent type is currently not available

The underlying symlink rejection is only visible in source/tests, not in the user-facing error.

Controlled A/B evidence

| Fresh task | cli_version recorded in rollout | Result |
| --- | --- | --- |
| 01a02a68-2edd-7d91-987b-acfd76e53f90 | 0.149.0 | Four symlink-installed configured-role spawn attempts were rejected with the exact error above |
| 01a02a6c-46a7-7101-9dc3-ba7f407ffb83 | 0.148.0 | Successfully spawned custom implementer, custom implementation_lead, and a nested custom implementer using fork_turns = "none" |
| 01a0250f-f83e-70e3-b2ee-f8a312e05999 | 0.148.0 | Historical successful custom-role workflow using the same installation style |

The 0.148.0 preflight was run only after both the CLI and the restarted remote app-server reported 0.148.0.

Sanitized rollout evidence:

fresh task 01a02a68-2edd-7d91-987b-acfd76e53f90
cli_version: 0.149.0
spawn result: agent type is currently not available

fresh task 01a02a6c-46a7-7101-9dc3-ba7f407ffb83
cli_version: 0.148.0
custom implementer: started
custom implementation_lead: started
nested custom implementer with fork_turns="none": started

All probes were read-only. No repository, release, configuration, or CODEX_HOME files were changed during the probes.

Related, but distinct reports

  • #26408 has the same public error text, but concerns project-scoped .codex/agents discovery on 0.137.0.
  • #20077 concerns full-history fork/override incompatibility. The successful nested control explicitly used fork_turns = "none".
  • #26868 concerns a child being created without its profile being applied. Here, 0.149.0 rejects the role before a child starts.
  • #40042 concerns a different bounded-role regression where model_instructions_file is silently ignored.

Suggested resolution

  • Surface a specific privacy-safe error such as:

``text
custom agent role file must be a regular file; symbolic links are not supported
``

  • Document the symlink restriction and the supported installation method in the custom-agent documentation.
  • Consider allowing symlinks after resolving and validating the target against an explicit trust boundary.
  • Add release-note coverage for the 0.149.0 compatibility change.
  • Keep a regression test for both the security rule and the user-facing diagnostic.

View original on GitHub ↗

1 Comment

Chang-Jin-Lee · 5 days ago

I bisected the 0.148.0 → 0.149.0 delta for this code path. The change that matches your A/B is #39299 (1a6e07a4fe, "Restrict agent roles to bounded configuration overrides"), which landed in 0.149.0.

What changed

load_role_layer_toml in codex-rs/core/src/agent/role.rs swapped the reader used for user-defined role files:

-        let role_config_contents = tokio::fs::read_to_string(config_file).await?;
+        let role_config_contents = read_sensitive_file_to_string(config_file).await?;

read_sensitive_file_to_string (added in #39200, codex-rs/exec-server/src/regular_file.rs:21) opens with O_NOFOLLOW and then rejects anything whose metadata says symlink. Its doc comment states the intent directly: "Reads a regular UTF-8 file without following a symlink at its final path component." The PR body lists it too — "reject symlinked user role files" — so the rejection itself is deliberate, and role_tests.rs:109 pins it.

The old reader followed symlinks. So a role file that is a symlink loaded fine on 0.148.0 and is refused on 0.149.0. Confirming the syscall-level difference on Linux:

| role file | 0.148.0 (read_to_string) | 0.149.0 (O_NOFOLLOW) |
| --- | --- | --- |
| implementer.toml → symlink | ok, 80 bytes | ELOOP |
| code_mapper.toml regular file | ok, 80 bytes | ok |

Only the final path component matters. A symlinked ~/.codex/agents/ directory still resolves; a symlinked *.toml inside it does not.

This also explains why only configured roles break. Built-in roles take the other branch in the same function and come from include_str! (role.rs:419), so they never touch the filesystem and never hit the new reader.

Why the error text gives you nothing

apply_role_to_config collapses every failure from apply_role_to_config_inner into one string, and the real cause only reaches a tracing::warn!:

apply_role_to_config_inner(config, role_name, &role)
    .await
    .map_err(|err| {
        tracing::warn!("failed to apply role to config: {err}");
        AGENT_TYPE_UNAVAILABLE_ERROR.to_string()
    })

A missing role file, a symlinked role file, and a role file with malformed TOML all surface as agent type is currently not available. Three tests in role_tests.rs assert exactly that, so the masking is locked in. That is the part I would call the actual defect here — the rejection may be intended, but it is currently undiagnosable from the client side.

Confirming it on your host

find ~/.codex/agents -maxdepth 1 -name '*.toml' -type l -printf '%p -> %l\n'

Anything listed there is refused on 0.149.0 and was accepted on 0.148.0. Dotfile managers that link role files into place (stow, chezmoi, a hand-rolled ln -s) produce exactly this layout, which fits a remote dev host.

If that command prints nothing, the cause is one of the other ? sites in the rewritten apply_role_to_config_inner rather than the reader. The warn line separates them:

codex -c log_dir=./.codex-log
tail -F ./.codex-log/codex-tui.log | grep 'failed to apply role to config'

I can run that on my side too if it would help narrow things down, though my roles are plain files so I would be reproducing your layout rather than my own.

Notes for a fix

Surfacing the underlying error to the caller would have turned this into a self-service diagnosis. Beyond that, the release notes for 0.149.0 list #39299 only in the raw commit list, not in the summarized sections, so a role layout that worked stopped working with no signal. CHANGELOG.md points at the releases page and carries nothing about it either.

Whether legitimate symlinked role files should keep working is a policy call I can't make from outside. The hardening blocks a real attack — the test target in regular_file_tests.rs writes model_provider = 'attacker' — but resolving the link and validating the target's ownership and location would preserve dotfile setups while keeping that closed.

Environment for the checks above: Linux, repo at 83d1fe0e67, tags rust-v0.148.0 and rust-v0.149.0. All read-only; I did not build or run codex itself.