A restricted shell interpreter for Go. Designed for AI agents that need to run shell commands safely.
go get github.com/DataDog/rshellpackage main
import (
"context"
"os"
"strings"
"time"
"github.com/DataDog/rshell/interp"
"mvdan.cc/sh/v3/syntax"
)
func main() {
script := `echo "hello from rshell"`
prog, _ := syntax.NewParser().Parse(strings.NewReader(script), "")
runner, _ := interp.New(
interp.StdIO(nil, os.Stdout, os.Stderr),
interp.AllowedCommands([]string{"rshell:echo"}),
interp.MaxExecutionTime(5*time.Second),
)
defer runner.Close()
runner.Run(context.Background(), prog)
}CLI usage also supports a whole-run timeout:
rshell --allow-all-commands --timeout 5s -c 'echo "hello from rshell"'Every access path is default-deny:
| Resource | Default | Opt-in |
|---|---|---|
| Command execution | All commands blocked (exit code 127) | AllowedCommands with namespaced command list (e.g. rshell:cat) |
| External commands | Blocked (exit code 127) | Provide an ExecHandler |
| Filesystem access | Blocked | Configure AllowedPaths with directory list |
| Environment variables | Empty (no host env inherited) | Pass variables via the Env option |
| Output redirections | Only /dev/null allowed (exit code 2 for other targets) |
>/dev/null, 2>/dev/null, &>/dev/null, 2>&1 |
AllowedCommands restricts which commands (builtins or external) the interpreter may execute. Commands must be specified with the rshell: namespace prefix (e.g. rshell:cat, rshell:echo). If not set, no commands are allowed.
AllowedPaths restricts all file operations to specified directories using Go's os.Root API (openat syscalls), making it immune to symlink traversal, TOCTOU races, and .. escape attacks.
Note: The
ssandip routebuiltins bypassAllowedPathsfor their/proc/net/*reads. Both builtins open kernel pseudo-filesystem paths (e.g./proc/net/tcp,/proc/net/route) directly withos.Openrather than going through the sandboxed opener. These paths are hardcoded in the implementation and are never derived from user input, so there is no sandbox-escape risk. However, operators cannot useAllowedPathsto blockssfrom enumerating local sockets orip routefrom reading the routing table — these reads succeed regardless of the configured path policy.
ProcPath (Linux-only) overrides the proc filesystem root used by the ps builtin (default /proc). This is a privileged option set at runner construction time by trusted caller code — scripts cannot influence it. Access to the proc path is intentionally not subject to AllowedPaths restrictions, since proc is a read-only virtual filesystem that does not expose host data under the normal file hierarchy.
See SHELL_FEATURES.md for the complete list of supported and blocked features.
Linux, macOS, and Windows.
900+ YAML-driven test scenarios cover builtins, shell features, and security restrictions.
tests/scenarios/
├── cmd/ # builtin command tests (echo, cat, grep, head, tail, test, uniq, wc, ...)
└── shell/ # shell feature tests (pipes, variables, control flow, ...)
By default, each scenario is executed twice: once in rshell and once in a real bash shell, ensuring output parity with POSIX behavior. Scenarios that test rshell-specific restrictions (blocked commands, readonly enforcement, etc.) opt out of the bash comparison.
go test ./...After merging changes to main create a release by:
-
Navigate to the Releases page
-
Click "Draft a new release"
-
You can "Select a tag" using the dropdown or "Create a new tag"
When creating a new tag, make sure to include the
vprefix. For example, if the last release was v0.1.29, your release should be v0.1.30. -
The release title should be the same as the version tag
-
Use "Generate release notes" to fill in the release description
-
Click "Publish release"
This will create a git tag that can now be referenced in other repos. This will trigger go-releaser that will add installable artifacts to the release.
