Repository navigation
Tabs and line breaks silently dropped from exec_command workdir and apply_patch paths (PathUri::join) #52688
Description
Activity
- addedCLIIssues related to the Codex CLIIssues related to the Codex CLItool-callsIssues related to tool callingIssues related to tool calling
on Oct 9, 2026 Confirmed independently on current
main(10da569252), and reproduced by running a test against
codex-utils-path-urirather than by reading:PathUri::from_str("file:///tmp/ws")?.join("config.txt\tx") => file:///tmp/ws/config.txtx // tab silently gone; join_descendant inherits it PathUri::from_str("file:///tmp/ws")?.join("a\nb") => file:///tmp/ws/ab // LF gone PathUri::from_str("file:///tmp/ws")?.join("a\rb") => file:///tmp/ws/ab // CR gone PathUri::from_str("file:///tmp/ws")?.join("a\0b") => Err(InvalidFileUriPath)Two things worth adding to your root cause:
1.
joinis inconsistent withfrom_abs_path, which already gets this right.AbsolutePathBuf::from_absolute_path("/tmp/ws/config.txt\tx") -> PathUri::from_abs_path => file:///tmp/ws/config.txt%09x and to_abs_path round-trips to the real "/tmp/ws/config.txt\tx"So the crate can already represent a control-character path — only the build path (
join) cannot
produce one. I'd treat that asymmetry, rather than "control characters are unsupported", as the bug
to fix: the fix should makejoinagree withfrom_abs_path. NUL being rejected up front
(InvalidFileUriPath) is the existing precedent if rejection is preferred over preservation.2. Pre-encoding the segment does not work.
PathSegmentsMut::pushpercent-encodes%itself, sobase.join("config.txt%09x") => file:///tmp/ws/config.txt%2509x // double-encodedThere is no input string that makes
pushemit%09. A fix therefore has to bypass the WHATWG path
setter for the affected components — percent-encode with the serializer's own encode set and
Url::parsethe result, or route through the same conversionfrom_abs_pathuses.
absolute_path_normalization.rs::path_uri_from_segmentshas the samesegments.extend(...)pattern,
so it is exposed the same way.I have a fix in progress along the "make
joinagree withfrom_abs_path" line and will open a PR
against the fork with a regression test.Reacted by Sishuai GongImplemented a fix along the "make
joinagree withfrom_abs_path" line and validated it against the
current tree. Details below, including two things I could only settle by running code.Both construction routes are affected, not just
join's relative branch.PathUri::parse("file:///workspace")?.join("config.txt\tx") => file:///workspace/config.txtx PathUri::parse("file:///workspace")?.join("/tmp/config.txt\tx") => file:///tmp/config.txtxThe second line takes the absolute-path branch through
absolute_path_normalization.rs::path_uri_from_segments, which has the samesegments.extend(...)
shape. Sinceexec_commandresolvesworkdirvianative_environment_cwd.join(workdir)
(exec_command.rs:192) andapply_patchviacwd.join(...)(parser.rs:90), both the relative and
absolute spellings hit the bug, so a fix has to cover both call sites.Only three characters are involved.
url::PathSegmentsMut::pushencodes everything else exactly
as you would want; tab, LF and CR are the only ones the WHATWG path state drops. I verified that
character-for-character againstUrl::from_file_path, which agrees with the setter for every
character tested (space,%,?,#,\,{,`,", non-ASCII, and the other C0 controls
0x01/0x07/0x1F, plus0x7F) and differs only for the three drops, where it emits%09/%0A/
%0D. That is what makes the fix narrow: encode those three, and leave every other character to the
setter so the resulting spelling does not move.A
%-based pre-encode cannot work, and neither can a post-hoc repair.pushpercent-encodes%
itself, so an already-escaped segment is double-encoded, and repairing the escapes afterwards cannot
distinguish a dropped tab from a literal%09the user typed. The fix therefore writes the encoded
segment to the path rather than pushing raw text.Fix (fork branch,
codex-rs/utils/path-urionly):- new
path_encoding.rs:encode_path_segmentsplits a segment into runs at tab/LF/CR, encodes each
run through the setter itself (so the spelling is unchanged for every other character), and joins
them with explicit%09/%0A/%0D; lib.rs::joinappendsencode_path_segment(component)to the encoded path instead of pushing the
raw component, keeping the existing../pop_if_emptyhandling untouched;absolute_path_normalization.rs::path_uri_from_segmentsbuilds the path from the same encoder.
Literal
%is unaffected:a%09bstill spellsa%2509b, so the existing
encoded_filename_characters_round_trip_without_becoming_uri_metadatabehavior is preserved. Note
thatjoin_native_bytesalready encoded its segments before writing the path — only the UTF-8 route
was losing them, which is why this reads as a gap rather than a design choice.Validation
cargo test -p codex-utils-path-uri— 86 passed (3 new regression tests: relative and absolute
joins across POSIX/Windows conventions, the round trip back to the native path with the real tab,
and the%09-stays-literal guard);cargo test -p codex-apply-patch— 97 passed;cargo clippy -p codex-utils-path-uri --all-targetsandcargo fmt --checkclean.
The change is on a fork branch, since this repository does not take external pull requests:
argszero#7- new
What issue are you seeing?
When a path contains a tab, LF or CR, Codex drops those characters and uses
a different path. If the workspace has a directory
config.txt<TAB>xandthe model runs
exec_commandwith"workdir": "config.txt\tx", Codexstarts the command in
config.txtx. That directory doesn't exist, so thecommand never starts. The model is told:
A process trace confirms the process-setup helper is started with cwd
<workspace>/config.txtx. apply_patch resolves its paths the same way, so afile
x.md<TAB>xis written asx.mdx.What steps can reproduce the bug?
mkdir "$(printf 'config.txt\tx')"exec_commandwith{"cmd": "ls", "workdir": "config.txt\tx"}(\tis a real tab in the JSON).What is the expected behavior?
The command runs in
config.txt<TAB>x, the directory the model named. Ifsuch paths aren't supported, Codex should refuse them with a clear error
instead of silently using another path. For comparison, on the same call
OpenCode runs in the real directory, and Gemini CLI rejects the path with
"Path contains invalid characters (newlines or control characters)".
Additional information
Root cause:
core/src/tools/handlers/unified_exec/exec_command.rs:203resolves
workdirwithPathUri::join, and so does apply_patch(
apply-patch/src/parser.rs:90).PathUri::join(
utils/path-uri/src/lib.rs:528) andpath_uri_from_segments(
utils/path-uri/src/absolute_path_normalization.rs:43) add each name withPathSegmentsMut::push/extend. Theurlcrate parses that input and,following the WHATWG URL spec, strips ASCII tab and newline from it.
Url::from_file_pathdoes not have this problem; it encodes a tab as%09.