hooks/builtins: name the resolved shell in the env block - #4072
Conversation
218d7eb to
4166ee0
Compare
c22e7f5 to
75969f7
Compare
The session_start env block previously named the OS but not the shell. When the model reads 'Operating System: Windows' it defaults to POSIX syntax by habit and emits '&&', 'grep', 'head', '2>/dev/null', 'ls -la', '$(pwd)' — none of which work under Windows PowerShell 5.1 or cmd.exe. The shell tool description already carries a shell-specific hint, but it sits far from the user turn and is drowned out by the agent's own system prompt; the env block sits at the top of every turn and is cached, so that is where the disambiguation belongs. Resolve the shell via the shared shellpath.DetectShell helper and print its base name plus the resolved path. Long-form dialect rules live in the shell tool description (shellSyntaxHint) and in the reactive self-correction hint (shellDialectHint, follow-up commit) — this block stays terse so it doesn't diverge from those and so the guidance stays correct as models improve. The base-name derivation moves to a new shellpath.ShellBaseName helper so the same logic doesn't get copied into hooks/builtins; the pre-existing copy in the shell toolset moves over here too. No behaviour change on darwin/linux where the shell defaults to $SHELL or /bin/sh; the extra 'Shell:' line lands cleanly in the existing tab-indented env block.
a6a2f90 to
465e895
Compare
Sayt-0
left a comment
There was a problem hiding this comment.
The Shell: line in the env block addresses a real gap: sessions on Windows repeatedly emit POSIX syntax under PowerShell 5.1 / cmd.exe, and the existing shellSyntaxHint sits too far from the model's attention. The approach (reuse shellpath.DetectShell, prompt-cached line) is sound. Build and tests pass on the branch.
Two items to resolve before merge:
| # | Issue | Files | Severity |
|---|---|---|---|
| 1 | ShellBaseName duplicates an existing private helper |
pkg/shellpath/shellpath.go, pkg/tools/builtin/shell/shell.go |
blocking |
| 2 | Doc comment references shellDialectHint, which does not exist in the repo |
pkg/hooks/builtins/add_environment_info.go |
blocking |
Details in the inline comments. Two minor nits included as well, take or leave.
Drop the private helper and its table test that duplicated pkg/shellpath.ShellBaseName. Prevents the two copies from drifting.
- Drop the stale reference to shellDialectHint, which does not exist. - Restore the note that addEnvironmentInfo no-ops when Cwd is empty. - Clarify that the discarded value from shellpath.DetectShell is argsPrefix, not an error.
aheritier
left a comment
There was a problem hiding this comment.
CI is green on 3609910 (build-and-test, lint, license-check, windows-tests, build-image linux/amd64+arm64, CodeQL go/js/actions), and all three commits are signed and verified. Verified independently on the head commit: go build ./..., GOOS=windows GOARCH=amd64 go build ./pkg/..., and go test ./pkg/shellpath/... ./pkg/hooks/builtins/... ./pkg/tools/builtin/shell/... all pass.
The change does what the description says, and the previous round is fully addressed:
shellpath.ShellBaseNameis now the single implementation:grep -rn '\bshellBaseName\b'returns nothing, both call sites (shell.go:371,shell.go:412) delegate, and the moved table test inpkg/shellpath/shellpath_test.go:10is a superset of the removed one (adds/bin/shand"").grep -rn shellDialectHintreturns nothing — the dead pointer is gone.- The "No-op when Cwd is empty." note and the
argsPrefixclarification are back. - The core invariant holds: the hook resolves via
shellpath.DetectShell(), the same source asshellHandler.shell(shell.go:294), so the env block cannot name a shell different from the one the tool executes. - The prompt-cache claim checks out —
AddEnvironmentInfooutput sits behind thecache_controlmarker (pkg/session/session.go:2227).
Findings
[should-fix] Docs enumerate the env block without the new shell field.
Two reference tables list the injected fields exhaustively and are now incomplete:
docs/configuration/hooks/index.md:209— "Adds the working directory, git-repo status, OS, and CPU architecture."docs/configuration/agents/index.md:96— "injects working directory, OS, CPU architecture, and git info into context."
Adding the resolved shell to both keeps them accurate in the same change.
[should-fix] 465e8955's commit body will land in main with stale references.
Squash merge is disabled on this repo (allow_squash_merge: false), so all three commit messages are permanent history. That body cites shellDialectHint — which does not exist in the repo — and "follow-up commit", which stops resolving once the message is in history; it also states "the pre-existing copy in the shell toolset moves over here too", which only became true in 715df7ba. The code comment was corrected in 3609910, but the commit body still carries the dead pointer. Worth a reword on the next rebase.
[optional] The env-block / shell-tool agreement isn't tested.
add_environment_info_test.go:54,61 builds the expected string from the same shellpath.DetectShell() and shellpath.ShellBaseName() calls production uses, so it pins the line's placement and format but cannot fail if the hook's resolution later diverges (e.g. someone swaps in DetectUnixShell) — which is the invariant this change exists to protect. It matches the existing displayOS()/displayArch() style in that test, so take or leave.
[optional] pkg/shellpath's package comment (shellpath.go:1) still scopes the package to "safe shell binary resolution to prevent PATH hijacking attacks (CWE-426)", while ShellBaseName is a pure formatting helper. A one-line broadening keeps the doc honest now that the package hosts both.
Leaving this as a comment rather than an approval so the two [should-fix] items — the docs enumerations and the 465e8955 commit body — can be picked up first; neither is a correctness problem, and the code itself looks good to me. Note that merge is separately gated by the earlier CHANGES_REQUESTED review, which resolving threads does not dismiss; the original reviewer needs to re-review or that review needs dismissing.
The problem
On Windows, docker-agent sessions repeatedly emit POSIX syntax —
&&,grep,head,2>/dev/null,ls -la,$(pwd)— under Windows PowerShell 5.1 or cmd.exe. Each attempt fails with a distinctive error, the model re-derives the dialect from scratch, and often emits the same wrong syntax on the next turn. Long sessions burn many tool calls this way.Why
The env block at the top of every session says:
Windowsalone doesn't disambiguate the shell (PowerShell 5.1? 7? cmd.exe? — different dialects). The model defaults to POSIX by habit since that's most of its training data. The shell tool description does carry a shell-specific hint (shellSyntaxHint), but it sits far from where the model is paying attention.The change
Add a
Shell:line to the env block, resolved via a newshellpath.ShellBaseNamehelper:Same treatment for
pwsh,cmd,zsh,bash, etc. Prompt-cached, so no per-turn cost.