diff --git a/docs/configuration/agents/index.md b/docs/configuration/agents/index.md
index 9da240a9cb..61163733e7 100644
--- a/docs/configuration/agents/index.md
+++ b/docs/configuration/agents/index.md
@@ -93,7 +93,7 @@ agents:
| `toolsets` | array | ✗ | List of tool configurations. See [Tool Config](../tools/index.md). |
| `fallback` | object | ✗ | Automatic model failover configuration. |
| `add_date` | boolean | ✗ | When `true`, injects the current date into the agent's context. |
-| `add_environment_info` | boolean | ✗ | When `true`, injects working directory, OS, CPU architecture, and git info into context. |
+| `add_environment_info` | boolean | ✗ | When `true`, injects working directory, OS, CPU architecture, git info, and the resolved shell into context. |
| `add_prompt_files` | array | ✗ | List of file paths whose contents are appended to the system prompt. Useful for including coding standards, guidelines, or additional context. |
| `add_description_parameter` | boolean | ✗ | When `true`, adds agent descriptions as a parameter in tool schemas. Helps with tool selection in multi-agent scenarios. |
| `redact_secrets` | boolean | ✗ | When `true`, scrubs detected secrets (API keys, tokens, private keys, etc.) out of tool-call arguments, outgoing chat messages, and tool output before they reach a tool, the model, or downstream consumers. See [Redacting Secrets](#redacting-secrets) below. |
diff --git a/docs/configuration/hooks/index.md b/docs/configuration/hooks/index.md
index 34efe9d2b8..c146ab4418 100644
--- a/docs/configuration/hooks/index.md
+++ b/docs/configuration/hooks/index.md
@@ -206,7 +206,7 @@ Built-ins are typically zero-config and faster than equivalent shell hooks becau
| Builtin | Event | Args | What it does |
| ----------------------- | ----------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `add_date` | `turn_start` | _none_ | Prepends `Today's date: YYYY-MM-DD` so the model always knows the current date. |
-| `add_environment_info` | `session_start` | _none_ | Adds the working directory, git-repo status, OS, and CPU architecture. |
+| `add_environment_info` | `session_start` | _none_ | Adds the working directory, git-repo status, OS, CPU architecture, and the resolved shell. |
| `add_prompt_files` | `turn_start` | `[file1, file2, ...]` | Reads each named file from the workdir hierarchy (walking up) and the home directory, and appends their contents. |
| `add_git_status` | `turn_start` | _none_ | Adds the output of `git status --short --branch` (no-op outside a git repo or when git isn't installed). |
| `add_git_diff` | `turn_start` | _none_, or `["full"]` | Adds `git diff --stat` by default. Pass `args: ["full"]` to emit the full unified diff. Output is capped to 4 KB. |
diff --git a/pkg/hooks/builtins/add_environment_info.go b/pkg/hooks/builtins/add_environment_info.go
index 9bf7a02ebd..2eafbfd03c 100644
--- a/pkg/hooks/builtins/add_environment_info.go
+++ b/pkg/hooks/builtins/add_environment_info.go
@@ -6,13 +6,14 @@ import (
"runtime"
"github.com/docker/docker-agent/pkg/hooks"
+ "github.com/docker/docker-agent/pkg/shellpath"
)
// AddEnvironmentInfo is the registered name of the add_environment_info builtin.
const AddEnvironmentInfo = "add_environment_info"
-// addEnvironmentInfo emits cwd / git / OS / arch info as session_start
-// additional context. No-op when Cwd is empty.
+// addEnvironmentInfo emits cwd/git/OS/arch/shell as session_start context.
+// No-op when Cwd is empty.
func addEnvironmentInfo(_ context.Context, in *hooks.Input, _ []string) (*hooks.Output, error) {
if in == nil || in.Cwd == "" {
return nil, nil
@@ -20,20 +21,22 @@ func addEnvironmentInfo(_ context.Context, in *hooks.Input, _ []string) (*hooks.
return hooks.NewAdditionalContextOutput(hooks.EventSessionStart, environmentInfo(in.Cwd)), nil
}
-// environmentInfo formats the env block injected at session_start:
-// working directory, git-repo status, and human-readable OS / arch.
+// environmentInfo builds the block. Long-form dialect rules live in
+// shellSyntaxHint (tool description) so this stays terse.
func environmentInfo(workingDir string) string {
gitRepo := "No"
if isGitRepo(workingDir) {
gitRepo = "Yes"
}
+ shellPath, _ := shellpath.DetectShell() // second value is argsPrefix, unused here
return fmt.Sprintf(`Here is useful information about the environment you are running in:
Working directory: %s
Is directory a git repo: %s
Operating System: %s
CPU Architecture: %s
- `, workingDir, gitRepo, displayOS(), displayArch())
+ Shell: %s (%s)
+ `, workingDir, gitRepo, displayOS(), displayArch(), shellpath.ShellBaseName(shellPath), shellPath)
}
// displayOS returns a friendlier label for the common values of
diff --git a/pkg/hooks/builtins/add_environment_info_test.go b/pkg/hooks/builtins/add_environment_info_test.go
index 973713b2dc..b2c5764aad 100644
--- a/pkg/hooks/builtins/add_environment_info_test.go
+++ b/pkg/hooks/builtins/add_environment_info_test.go
@@ -7,6 +7,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/docker/docker-agent/pkg/shellpath"
)
func TestEnvironmentInfo(t *testing.T) {
@@ -49,12 +51,14 @@ func TestEnvironmentInfo(t *testing.T) {
if tt.expectGit {
gitStatus = "Yes"
}
+ shellPath, _ := shellpath.DetectShell()
expected := `Here is useful information about the environment you are running in:
Working directory: ` + dir + `
Is directory a git repo: ` + gitStatus + `
Operating System: ` + displayOS() + `
CPU Architecture: ` + displayArch() + `
+ Shell: ` + shellpath.ShellBaseName(shellPath) + ` (` + shellPath + `)
`
assert.Equal(t, expected, environmentInfo(dir))
diff --git a/pkg/shellpath/shellpath.go b/pkg/shellpath/shellpath.go
index 3b5fd56873..9d4914f0d4 100644
--- a/pkg/shellpath/shellpath.go
+++ b/pkg/shellpath/shellpath.go
@@ -7,8 +7,19 @@ import (
"os/exec"
"path/filepath"
"runtime"
+ "strings"
)
+// ShellBaseName returns the lowercase shell name without extension. Splits on
+// both separators so results are stable when the path came from another host OS.
+func ShellBaseName(shellPath string) string {
+ base := shellPath
+ if i := strings.LastIndexAny(base, `/\`); i >= 0 {
+ base = base[i+1:]
+ }
+ return strings.ToLower(strings.TrimSuffix(base, filepath.Ext(base)))
+}
+
// WindowsCmdExe returns the absolute path to cmd.exe on Windows using the
// SystemRoot environment variable (e.g. C:\Windows\System32\cmd.exe).
// This avoids resolving cmd.exe through PATH, which would be vulnerable
diff --git a/pkg/shellpath/shellpath_test.go b/pkg/shellpath/shellpath_test.go
index e66b928beb..77398380f1 100644
--- a/pkg/shellpath/shellpath_test.go
+++ b/pkg/shellpath/shellpath_test.go
@@ -7,6 +7,32 @@ import (
"testing"
)
+func TestShellBaseName(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ path string
+ expected string
+ }{
+ {path: "/bin/zsh", expected: "zsh"},
+ {path: "/usr/local/bin/fish", expected: "fish"},
+ {path: "/bin/sh", expected: "sh"},
+ {path: `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`, expected: "powershell"},
+ {path: `C:\Program Files\PowerShell\7\pwsh.exe`, expected: "pwsh"},
+ {path: `C:\Windows\System32\cmd.exe`, expected: "cmd"},
+ {path: "", expected: ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.path, func(t *testing.T) {
+ t.Parallel()
+ if got := ShellBaseName(tt.path); got != tt.expected {
+ t.Errorf("ShellBaseName(%q) = %q, want %q", tt.path, got, tt.expected)
+ }
+ })
+ }
+}
+
func TestWindowsCmdExe_ComSpec(t *testing.T) {
t.Setenv("ComSpec", `C:\Custom\cmd.exe`)
got := WindowsCmdExe()
diff --git a/pkg/tools/builtin/shell/shell.go b/pkg/tools/builtin/shell/shell.go
index 6c578fc8ee..532df5549a 100644
--- a/pkg/tools/builtin/shell/shell.go
+++ b/pkg/tools/builtin/shell/shell.go
@@ -368,7 +368,7 @@ func (t *ToolSet) Instructions() string {
- Use "cwd" parameter instead of cd within commands
- Combine operations with pipes, redirections, and heredocs
- Non-zero exit codes return error info with output; timed-out commands are terminated`,
- shellBaseName(t.handler.shell), displayOS())
+ shellpath.ShellBaseName(t.handler.shell), displayOS())
}
func (t *ToolSet) Tools(context.Context) ([]tools.Tool, error) {
@@ -409,7 +409,7 @@ func (t *ToolSet) Stop(context.Context) error {
// resolved shell is PowerShell or cmd.exe (e.g. "pwd && ls -la" is a
// parse error under Windows PowerShell 5.1).
func shellToolDescription(shellPath string) string {
- name := shellBaseName(shellPath)
+ name := shellpath.ShellBaseName(shellPath)
desc := fmt.Sprintf("Executes the given shell command with %s on %s.", name, displayOS())
if hint := shellSyntaxHint(name); hint != "" {
desc += " " + hint
@@ -435,18 +435,6 @@ func shellSyntaxHint(name string) string {
}
}
-// shellBaseName reduces a resolved shell path to a lowercase name the
-// model can recognize (C:\...\powershell.exe -> powershell, /bin/zsh -> zsh).
-// Splits on both separators instead of filepath.Base so the result is
-// deterministic regardless of the host OS the path came from.
-func shellBaseName(shellPath string) string {
- base := shellPath
- if i := strings.LastIndexAny(base, `/\`); i >= 0 {
- base = base[i+1:]
- }
- return strings.ToLower(strings.TrimSuffix(base, filepath.Ext(base)))
-}
-
// displayOS returns a friendlier label for the common values of
// runtime.GOOS, falling back to GOOS itself for anything exotic.
func displayOS() string {
diff --git a/pkg/tools/builtin/shell/shell_test.go b/pkg/tools/builtin/shell/shell_test.go
index 00bfb2f60f..bf017c3407 100644
--- a/pkg/tools/builtin/shell/shell_test.go
+++ b/pkg/tools/builtin/shell/shell_test.go
@@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/docker/docker-agent/pkg/config"
+ "github.com/docker/docker-agent/pkg/shellpath"
"github.com/docker/docker-agent/pkg/tools"
)
@@ -200,7 +201,7 @@ func TestShellTool_Instructions(t *testing.T) {
instructions := tool.Instructions()
assert.Contains(t, instructions, "Shell Tools")
- assert.Contains(t, instructions, shellBaseName(tool.handler.shell),
+ assert.Contains(t, instructions, shellpath.ShellBaseName(tool.handler.shell),
"instructions must name the resolved shell so the model uses its syntax")
assert.Contains(t, instructions, displayOS())
assert.NotContains(t, instructions, "run_background_job")
@@ -219,32 +220,10 @@ func TestShellTool_DescriptionNamesInterpreter(t *testing.T) {
require.Len(t, allTools, 1)
description := allTools[0].Description
- assert.Contains(t, description, shellBaseName(tool.handler.shell))
+ assert.Contains(t, description, shellpath.ShellBaseName(tool.handler.shell))
assert.Contains(t, description, displayOS())
}
-func TestShellBaseName(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- path string
- expected string
- }{
- {path: "/bin/zsh", expected: "zsh"},
- {path: "/usr/local/bin/fish", expected: "fish"},
- {path: `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`, expected: "powershell"},
- {path: `C:\Program Files\PowerShell\7\pwsh.exe`, expected: "pwsh"},
- {path: `C:\Windows\System32\cmd.exe`, expected: "cmd"},
- }
-
- for _, tt := range tests {
- t.Run(tt.path, func(t *testing.T) {
- t.Parallel()
- assert.Equal(t, tt.expected, shellBaseName(tt.path))
- })
- }
-}
-
func TestResolveWorkDir(t *testing.T) {
t.Parallel()