The run_bash tool

Shell is the escape hatch. Any test runner, linter, formatter, or compile step the agent needs can be reached through one well-built shell tool. That same escape hatch is also the most dangerous tool you will wire up, which is why the rest of the module is about containment.

agent/tools.go
go
{
	Spec: ToolSpec{
		Type: "function",
		Function: FunctionSpec{
			Name:        "run_bash",
			Description: "Run a shell command and return combined stdout+stderr. Dangerous commands are blocked.",
			Parameters: map[string]any{
				"type": "object",
				"properties": map[string]any{
					"cmd": map[string]any{"type": "string", "description": "Command line to execute with /bin/sh -c."},
				},
				"required": []string{"cmd"},
			},
		},
	},
	Run: runBash,
},

One field, cmd, executed through /bin/sh -c. This gives the model pipelines, globbing, and redirection. The description is explicit that some commands are blocked so the model does not keep retrying them.

agent/tools.go
go
cmd := exec.Command("/bin/sh", "-c", in.Cmd)
cmd.Env = os.Environ()

done := make(chan struct{})
var out []byte
var err error
go func() {
	out, err = cmd.CombinedOutput()
	close(done)
}()
select {
case <-done:
case <-time.After(30 * time.Second):
	_ = cmd.Process.Kill()
	return "", fmt.Errorf("run_bash: timed out after 30s")
}

cmd.CombinedOutput merges stdout and stderr, which matches how the model expects to read test runner output. A goroutine plus a select implements the timeout without pulling in a context library.

Quiz: Quiz

Loading practice…