Skip to content

feat: support in-repo .codex/config.toml entries as sources of config info#8354

Merged
bolinfest merged 1 commit intomainfrom
pr8354
Dec 22, 2025
Merged

feat: support in-repo .codex/config.toml entries as sources of config info#8354
bolinfest merged 1 commit intomainfrom
pr8354

Conversation

@bolinfest
Copy link
Collaborator

@bolinfest bolinfest commented Dec 20, 2025

  • We now support .codex/config.toml in repo (from cwd up to the first .git found, if any) as layers in ConfigLayerStack. A new ConfigLayerSource::Project variant was added to support this.
  • In doing this work, I realized that we were resolving relative paths in config.toml after merging everything into one toml::Value, which is wrong: paths should be relativized with respect to the folder containing the config.toml that was deserialized. This PR introduces a deserialize/re-serialize strategy to account for this in resolve_config_paths(). (This is why Serialize is added to so many types as part of this PR.)
  • Added tests to verify this new behavior.

Stack created with Sapling. Best reviewed with ReviewStack.

Copy link
Collaborator

@joshka-oai joshka-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main two comments are:

Do we want to allow a .codex folder with no config?
https://github.com/openai/codex/pull/8354/changes#r2637386529

Would this fail on the root of a windows drive?https://github.com/openai/codex/pull/8354/changes#r2637390433

The rest are observations / nits / typos / some future ideas as well as requests for docs to make understanding why this code is there a bit easier for future devs / agents.

Approval subject to the answers to the main questions being ok. I'd suggest updating docs and typos at a minimum (doesn't need a re-review)

Comment on lines 264 to 271
let Ok(resolved) = value.clone().try_into::<ConfigToml>() else {
return Ok(value);
};
drop(_guard);
let resolved_value = TomlValue::try_from(resolved).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to serialize resolved config: {e}"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(docs)
I kind of don't understand what this is doing / why. Converting from a toml value to a config toml value, then back to a toml value, but with a guard around it (which I'm assuming applies to the thread?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments here and in codex-rs/utils/absolute-path/src/lib.rs.

Ok(copy_shape_from_original(&value, &resolved_value))
}

fn copy_shape_from_original(original: &TomlValue, resolved: &TomlValue) -> TomlValue {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(docs)
This could do with a doc comment explaining input -> output and purpose.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments and a unit test in this file.


async fn find_project_root(cwd: &AbsolutePathBuf) -> io::Result<AbsolutePathBuf> {
for ancestor in cwd.as_path().ancestors() {
let git_dir = ancestor.join(".git");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Not a blocker)

https://github.com/openai/codex/pull/8359/changes adds various other markers for this.
I think we should probably also add some specific roots to this instead of discovering them.
E.g.:

  • the current git worktree (from git worktree list
  • the current jj workspace (from jj workspace root or jj workspace list)
  • other vcs roots that give programmatic access

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed via Slack: we're going to keep this feature SCM-agnostic right now and do not want to confuse users that our SCM layer is "pluggable." Features like undo are tied to Git today.

/// [ConfigLayerSource::Project] as the source) between `cwd` and
/// `project_root`, inclusive. The list is ordered in _increasing_ precdence,
/// starting from folders closest to `project_root` (which is the lowest
/// precedence) to those closest to `cwd` (which si the highest precedence).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(typo)

Suggested change
/// precedence) to those closest to `cwd` (which si the highest precedence).
/// precedence) to those closest to `cwd` (which is the highest precedence).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, failed by codespell.

Ok(())
}

fn resolve_config_paths(value: TomlValue, base_dir: &Path) -> io::Result<TomlValue> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(docs)
Please add a small doc comment here to clarify intent of this method.
What does resolving paths do?
Why is it paths not path?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to resolve_relative_paths_in_config_toml(). The "paths" are the referenced paths in the file.

Comment on lines 366 to 400
if err.kind() != io::ErrorKind::NotFound {
return Err(io::Error::new(
err.kind(),
format!(
"Failed to read project config file {}: {err}",
config_file.as_path().display(),
),
));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the point where fs-err might come in handy for reducing a bit of code - not a big deal and if we do it probably worth doing as a general codex cleanup.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree it seems more appropriate for a general code cleanup.

));
}
Err(err) => {
if err.kind() != io::ErrorKind::NotFound {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(blocking question)
Do we want to fail when a .codex folder exists without a config file in it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say no: ultimately one could have .codex/skills without .codex/config.toml, I believe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only return an error when err is NOT ENOENT, so I don't believe we are failing in the current case.

Though we should still return a layer when err is ENOENT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the logic and added project_layer_is_added_when_dot_codex_exists_without_config_toml() test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I fixed this in load_project_layers(), but not for ConfigLayerSource::User.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix in #8456.

dot_codex_folder: current,
} = &layer.name
{
if let Some(previous) = previous_project {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is previous higher or lower precedence in this method? (requires reading the entire method to guess about this, making it difficult to judge the correctness of this change)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment.

@bolinfest bolinfest merged commit 8ff16a7 into main Dec 22, 2025
71 of 73 checks passed
@bolinfest bolinfest deleted the pr8354 branch December 22, 2025 19:07
@github-actions github-actions bot locked and limited conversation to collaborators Dec 22, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants