Skip to content

feat(pwd): add pwd builtin with -L/-P symlink resolution#208

Draft
julesmcrt wants to merge 8 commits intomainfrom
jules.macret/implement-pwd-builtin
Draft

feat(pwd): add pwd builtin with -L/-P symlink resolution#208
julesmcrt wants to merge 8 commits intomainfrom
jules.macret/implement-pwd-builtin

Conversation

@julesmcrt
Copy link
Copy Markdown
Collaborator

Summary

  • Adds the POSIX pwd builtin with -L (logical, default), -P (physical, resolves symlinks), and --help flags. Last-wins semantics when -L/-P both appear (matches POSIX/bash).
  • Symlink resolution for -P walks the absolute path through callCtx.LstatFile/ReadlinkFile so it never bypasses AllowedPaths. Caps expansions at the Linux ELOOP limit (40 hops). Cycles error with exit 1; sandbox-blocked components pass through opaquely so -P stays useful when the cwd is the sandbox root.
  • Comprehensive test coverage: black-box, internal/whitebox, Unix-only symlink tests, pentest hardening tests, and 3 fuzz functions wired into .github/workflows/fuzz.yml. 94% line coverage on pwd.go.

Test plan

  • go test ./builtins/pwd/... — pass, 94% coverage
  • go test ./... — all packages pass
  • go test -run Fuzz -count=1 ./builtins/pwd/ — seed corpus passes
  • go test -fuzz=FuzzPwdArgs -fuzztime=20s — local fuzz run, no findings beyond the filtered upstream parser quirks
  • go run ./cmd/rshell --allow-all-commands -c 'help pwd' — help output appears correctly
  • go run ./cmd/rshell --allow-all-commands --allowed-paths /tmp -c 'pwd; pwd -L; pwd -P' — manual smoke test through a /tmp symlink confirms -P resolves and -L preserves

🤖 Generated with Claude Code

julesmcrt and others added 4 commits April 30, 2026 14:38
Adds the POSIX pwd builtin to print the absolute pathname of the current
working directory. Supports -L (logical, default) and -P (physical),
last-wins semantics when both are given, and --help. Symlink resolution
for -P is sandbox-safe: walks the path through callCtx.LstatFile and
ReadlinkFile, caps expansions at the Linux ELOOP limit (40 hops), and
treats components above AllowedPaths as opaque pass-through so -P stays
useful when the cwd lives at the sandbox root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Windows, paths constructed from `string(filepath.Separator)` alone
yield `\foo`, which filepath.IsAbs rejects — Windows requires a drive
letter. Introduce an `absRoot` helper that returns "/" on Unix and
`C:\` on Windows, and use it everywhere the internal tests need an
absolute root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A relative symlink target like "./real" uses forward slashes, but the
walking loop in resolveSymlinks splits on filepath.Separator — `\` on
Windows. Without normalization the target is treated as one opaque
component and the resolved path ended up containing a literal "./"
segment (e.g. `C:\./real` instead of `C:\real`).

filepath.Clean collapses "." segments and converts "/" to "\" on
Windows, so the splice produces a correctly-tokenized rest for the
resolver to walk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Windows pwd output uses backslash separators, so the existing
`stdout_contains: ["/"]` assertion fails on the windows-latest runner.
Add `stdout_contains_windows: ["\\\\"]` to every pwd scenario that
asserts a substring of the printed path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c93caf2f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/pwd/pwd.go Outdated
Comment thread builtins/pwd/pwd.go Outdated
Two bugs surfaced by the Codex review:

1. pflag.FlagSet.Visit walks set flags in lexicographical (or
   declaration) order, never command-line order. The previous
   pickPhysical helper therefore picked the wrong mode whenever
   `-P -L` appeared in that order — for a symlinked cwd, `pwd -P -L`
   printed the physical path even though POSIX (and bash) say -L wins.

2. pflag.BoolP accepts explicit values like `--physical=false`. The
   handler used to treat any visited mode flag as true, so
   `pwd --physical=false` silently selected physical mode. GNU
   `/bin/pwd --physical=false` rejects the argument; we should too.

Switch -L/-P to a shared boolSeqFlag (matching the head builtin's
boolSeqFlag pattern). The flag's Set increments a shared counter and
records the new value in pos, so we can compare pos fields after
parsing to determine the actual command-line order. Set also rejects
any value other than the NoOptDefVal "true", which makes
`--physical=false` and friends fail with "option doesn't allow an
argument".

Add regression tests with a real symlinked cwd (the previous tests
used a non-symlink temp dir, so the wrong mode was hidden), and
internal tests asserting that explicit values are rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ba4c37883

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/pwd/pwd.go
Comment thread builtins/pwd/pwd.go Outdated
@julesmcrt julesmcrt added the verified/analysis Human-reviewed static analysis changes label Apr 30, 2026
Address two more findings from the Codex review:

1. P2: Container-style sandboxes mount the host filesystem at a prefix
   (e.g. /mnt/host) and on-disk symlinks often store host-absolute
   targets (e.g. /var/log/pods/app) without that prefix. The previous
   pwd -P resolver took the readlink string at face value and tried to
   walk it relative to root, so for a cwd reached via such a symlink
   the printed path was unreachable through the sandbox — `cat $(pwd)`
   would fail even though the file is allowed.

   Expose the runner's host prefix on CallContext and apply it to
   absolute symlink targets that don't already start with it. The
   resolved path now matches what the sandbox itself accepts.

2. P3: pwd --physical=true was silently accepted because pflag passed
   the literal "true" both for the bare flag (via NoOptDefVal="true")
   and for the explicit `=true` form, so boolSeqFlag.Set could not
   distinguish them. Switch NoOptDefVal to a private sentinel string
   so any explicit value — including "true" — fails Set's equality
   check and is rejected as "option doesn't allow an argument",
   matching GNU /bin/pwd.

Tests: add unix tests for the HostPrefix translation (with and without
double-prefix avoidance) and internal tests asserting that
--logical=true / --physical=true are now rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3cd881015

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/pwd/pwd.go Outdated
Codex flagged that resolveSymlinks can return context.Canceled or
context.DeadlineExceeded when the run is interrupted mid-walk, but
the handler treated every non-loop error as a sandbox-miss and fell
back to printing the logical path with exit 0. RULES.md requires
graceful handling of cancellation, and reporting success with a
stale path is misleading.

Add a context-cancel check between the loop-error branch and the
silent fallback: if ctx.Err() is set, exit 1 without writing
anything. The sandbox-miss best-effort case (resolver returned a
non-loop, non-cancel error) keeps its logical-path fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 775b92ee5b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/pwd/pwd.go
Comment on lines +291 to +292
if hp := callCtx.HostPrefix(); hp != "" && !strings.HasPrefix(cleanedTarget, hp+string(filepath.Separator)) && cleanedTarget != hp {
cleanedTarget = filepath.Join(hp, cleanedTarget)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize HostPrefix before prefix checks

When callers configure HostPrefix with a trailing separator (for example HostPrefix("/mnt/host/")), this code compares the raw prefix against a cleaned symlink target. Absolute symlink targets that already include the cleaned prefix then fail the HasPrefix check and get prefixed again, so pwd -P can print paths like /mnt/host/mnt/host/real even though the sandbox itself normalizes the prefix. Cleaning hp before the comparison/join keeps pwd -P consistent with AllowedPaths resolution.

Useful? React with 👍 / 👎.

@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 775b92ee5b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/pwd/pwd.go
Comment on lines +291 to +292
if hp := callCtx.HostPrefix(); hp != "" && !strings.HasPrefix(cleanedTarget, hp+string(filepath.Separator)) && cleanedTarget != hp {
cleanedTarget = filepath.Join(hp, cleanedTarget)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize HostPrefix before applying it

When HostPrefix is configured with a trailing slash or ./.. components, the sandbox normalizes it via SetHostPrefix, but pwd receives the raw value and uses it in this prefix check. In a container setup where the symlink target already includes the cleaned prefix, for example HostPrefix("/mnt/host/") and target /mnt/host/var, hp + separator becomes /mnt/host//, so the check misses and filepath.Join produces /mnt/host/mnt/host/var; pwd -P then prints a non-existent double-prefixed path. Clean hp, or return the sandbox's normalized prefix, before comparing/joining.

Useful? React with 👍 / 👎.

Codex flagged that interp.HostPrefix("/mnt/host/") (trailing slash)
breaks pwd -P: the sandbox normalizes the prefix via filepath.Clean
in SetHostPrefix, but my CallContext callback returned the runner's
raw r.hostPrefix. The unnormalized value defeats the HasPrefix
check ("/mnt/host//" never matches a clean target) and filepath.Join
then doubles the prefix, producing paths like
"/mnt/host/mnt/host/var".

Switch both CallContext.HostPrefix sites to call r.sandbox.HostPrefix(),
which already returns the filepath.Clean'd value. The raw runner
field stays as-is (still consumed by SetHostPrefix during init).

Add a regression test that passes HostPrefix with a trailing slash
and verifies the resolved path is not double-prefixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 869e4caf85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/pwd/pwd.go
}

candidate := joinPath(out, comp)
info, err := callCtx.LstatFile(ctx, candidate)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve symlinked sandbox roots in pwd -P

When the working directory is itself under an AllowedPaths root that is a symlink (for example launching from /tmp/link and passing --allowed-paths /tmp/link), pwd -P still prints the logical /tmp/link path. The sandbox opens roots through os.OpenRoot, so this LstatFile call sees the opened root's target directory rather than the symlink path and the branch below treats it as non-symlink; this makes the documented physical mode fail in the common -p "$PWD" configuration after entering a directory via a symlink.

Useful? React with 👍 / 👎.

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

Labels

verified/analysis Human-reviewed static analysis changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant