Field notes from working through example 24 of Ardan Labs’ Ultimate AI course by Bill Kennedy and Florin Pățan (Apache 2.0). My fork: PratikDhanave/ai-training. Thank you Bill and Florin for teaching this material — the patterns in this post are derived from the course; the production reflections at the end are mine.
What the example teaches
The naive shell tool:
func runCommand(args string) string {
out, _ := exec.Command("sh", "-c", args).Output()
return string(out)
}
The hardened version:
- Allow-list of binaries the tool can invoke (
ls,grep, specific scripts). - Argument scrubbing (no shell metachars, no pipes, no redirects).
- Per-user RBAC: user X can run binary Y but not Z.
- Wall-time cap and output size cap.
- Audit log per invocation.
What it looks like
type ShellTool struct {
AllowedBinaries map[string]bool
Permissions func(user, binary string) bool
MaxDuration time.Duration
MaxOutputBytes int
Audit AuditLog
}
func (t *ShellTool) Run(ctx context.Context, user, binary string, args []string) (string, error) {
if !t.AllowedBinaries[binary] {
return "", ErrBinaryNotAllowed
}
if !t.Permissions(user, binary) {
return "", ErrPermissionDenied
}
if err := validateArgs(args); err != nil {
return "", err
}
ctx, cancel := context.WithTimeout(ctx, t.MaxDuration)
defer cancel()
out, err := exec.CommandContext(ctx, binary, args...).Output()
t.Audit.Log(user, binary, args, len(out), err)
return truncate(string(out), t.MaxOutputBytes), err
}
What I learned
sh -c "$user_input" is never the right answer. Always invoke the binary directly with separated argv. The temptation to use shell for “convenience” (pipes, redirects, env vars) is the temptation that gets the system owned.
RBAC per binary is more useful than RBAC per user. A user might be allowed to run grep but not rm. The grain matters.
Production connection
The Genie tool-calling layer applies a similar pattern — pkg/safety plugin chain runs against every tool invocation, including binary allow-list + argument validation. The coding agent example (#31) without these guards is a demo; with them, it’s something you can actually run against your own filesystem.
Credit & reference. This post is field notes on example 24 from Ardan Labs’ Ultimate AI by Bill Kennedy + Florin Pățan, licensed Apache 2.0. The original example: cmd/examples/example24-tool-security/. My fork with notes: PratikDhanave/ai-training. Highly recommend the course for anyone building AI applications in Go — the material is rigorous and the Kronk + yzma + llama.cpp pipeline gives you hardware-accelerated local inference end-to-end. Thank you, Bill and Florin.