Skip to content
This repository was archived by the owner on Apr 19, 2026. It is now read-only.

Commit cc2d1d0

Browse files
jpleva91claude
andcommitted
feat: governed shell wrapper for Goose + fix catch-all deny bug
govern-shell.sh: replaces /bin/sh as SHELL when running Goose. Every command is evaluated through `shellforge evaluate` before execution. Denied commands return error to agent for self-correction. `shellforge run goose` automatically sets SHELL to govern-shell.sh. Also fixed critical bug: bounded-execution policy had action:deny with command:"*" which denied ALL tool calls. Changed to action:monitor. This was the root cause of empty directory results and tool failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b37ce23 commit cc2d1d0

3 files changed

Lines changed: 64 additions & 2 deletions

File tree

agentguard.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ policies:
4242
match:
4343
command: "*"
4444
timeout_seconds: 300
45-
action: deny
46-
message: "Agent exceeded 5-minute execution limit"
45+
action: monitor
46+
message: "Agent execution timeout tracking"
4747

4848
telemetry:
4949
enabled: true

cmd/shellforge/main.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,32 @@ func cmdRun(driver, prompt string) {
557557
cmd.Stdin = os.Stdin
558558
cmd.Stdout = os.Stdout
559559
cmd.Stderr = os.Stderr
560+
cmd.Env = os.Environ()
561+
562+
// For Goose: set governed shell so ALL commands go through AgentGuard
563+
if driver == "goose" {
564+
sfBin, _ := exec.LookPath("shellforge")
565+
if sfBin != "" {
566+
// Find govern-shell.sh next to the shellforge binary or in known locations
567+
governShell := ""
568+
for _, path := range []string{
569+
filepath.Join(filepath.Dir(sfBin), "..", "share", "shellforge", "govern-shell.sh"),
570+
filepath.Join(filepath.Dir(sfBin), "govern-shell.sh"),
571+
"scripts/govern-shell.sh",
572+
"/usr/local/share/shellforge/govern-shell.sh",
573+
} {
574+
if _, err := os.Stat(path); err == nil {
575+
governShell = path
576+
break
577+
}
578+
}
579+
if governShell != "" {
580+
cmd.Env = append(cmd.Env, "SHELL="+governShell)
581+
cmd.Env = append(cmd.Env, "SHELLFORGE_REAL_SHELL=/bin/bash")
582+
fmt.Println("[shellforge] governance: shell wrapper active — every command evaluated")
583+
}
584+
}
585+
}
560586

561587
if err := cmd.Run(); err != nil {
562588
if exitErr, ok := err.(*exec.ExitError); ok {

scripts/govern-shell.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
# govern-shell.sh — Governed shell for ShellForge
3+
#
4+
# Every command is evaluated by AgentGuard before running.
5+
# Set as SHELL for governed agents (Goose, etc.)
6+
7+
set -euo pipefail
8+
9+
REAL_SHELL="${SHELLFORGE_REAL_SHELL:-/bin/bash}"
10+
11+
# Fall through if shellforge not installed or no policy
12+
if ! command -v shellforge &>/dev/null; then
13+
exec "$REAL_SHELL" "$@"
14+
fi
15+
if [ ! -f "agentguard.yaml" ] && [ ! -f "../agentguard.yaml" ]; then
16+
exec "$REAL_SHELL" "$@"
17+
fi
18+
19+
# Handle -c flag (how subprocesses call shells)
20+
if [ "${1:-}" = "-c" ]; then
21+
shift
22+
COMMAND="$*"
23+
24+
# Evaluate through AgentGuard
25+
RESULT=$(printf '{"tool":"run_shell","action":"%s","path":"."}' "$COMMAND" | shellforge evaluate 2>/dev/null || echo '{"allowed":true}')
26+
27+
if echo "$RESULT" | grep -q '"allowed":false'; then
28+
REASON=$(echo "$RESULT" | sed 's/.*"reason":"\([^"]*\)".*/\1/')
29+
echo "[AgentGuard] DENIED: $REASON" >&2
30+
exit 1
31+
fi
32+
33+
exec "$REAL_SHELL" -c "$COMMAND"
34+
else
35+
exec "$REAL_SHELL" "$@"
36+
fi

0 commit comments

Comments
 (0)