cmd

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2026 License: MIT Imports: 19 Imported by: 1

README

🐚 cmd — Starlark module for executing external commands

codecov binary footprint

The cmd module lets Starlark scripts run external programs across Windows, macOS, and Linux. Execution is always via argv (exec.Command), never through a shell, and the module is disabled by default — the Go host must opt in with an allowlist before any command can run.

Security model (read this first)

cmd is deny-by-default and the policy is set by the Go host, never by the script or by environment variables:

  1. Disabled by default. A module from NewModule() refuses every run() call. The host enables execution with cmd.NewModuleWithAllow(...).
  2. Allowlist, deny-by-default. Once enabled, a command runs only if its canonical form (argv joined by single spaces) matches an allowlist prefix at a word boundary: "git" permits git status but not gitleaks; "go test" permits go test ./... but not go build. An empty allowlist permits nothing.
  3. No shell interpolation. Commands are split into argv and executed directly — there is no /bin/sh -c. Shell features like $VAR, &&, |, globbing, and ; are not interpreted; pass environment via env= and run one program per call.
  4. Unicode hardening. Command text is rejected if it contains control characters or Unicode format/zero-width characters (e.g. zero-width space, BiDi overrides, BOM), which could otherwise hide or reorder text to slip past the allowlist.
  5. Explicit allow-all escape hatch. cmd.NewModuleWithAllowAll() enables the module and bypasses the allowlist entirely — every command runs. It is the deliberate "dangerous, run anything" path for a host that has already decided the caller is fully trusted (e.g. a CLI behind a --dangerously-allow-all style flag). The input hardening above (argv-only, no shell, Unicode rejection) still applies; only the allowlist is skipped. Prefer NewModuleWithAllow with a specific allowlist whenever the command set is known.

The enable flag, allowlist, and allow-all decision are not configuration keys — they are set in Go via NewModuleWithAllow / NewModuleWithAllowAll and cannot be widened by a script or environment variable.

Cross-platform note

Because there is no shell, the first token must be a real executable on PATH for the current OS. Shell built-ins are not available as argv:

  • Windows dir, and echo as a cmd.exe builtin, are not runnable directly — use real binaries (go, git, where, …) or invoke the interpreter explicitly if you allowlist it.
  • echo is a real binary on most Unix systems but not on Windows.

Prefer real cross-platform tools (e.g. go, git) in portable scripts.

Installation

go get github.com/starpkg/cmd

Quick Start

Construct the module with an allowlist in Go, wire it into a Starlet interpreter, then load("cmd", …) from a script:

package main

import (
    "fmt"

    "github.com/1set/starlet"
    "github.com/starpkg/cmd"
)

func main() {
    // Enable execution with an allowlist (deny-by-default).
    cmdModule := cmd.NewModuleWithAllow("go", "git status")
    interpreter := starlet.New(
        starlet.WithModuleLoader("cmd", cmdModule.LoadModule()),
    )

    script := `
load("cmd", "run", "which")

# Look up an executable without running anything.
print("go at:", which("go"))

# Run an allowlisted command and inspect the result.
result = run("go version")
print("ok:", result.success, "code:", result.exit_code)
print(result.stdout)
`

    if err := interpreter.ExecScript("example.star", script); err != nil {
        fmt.Println("Error:", err)
    }
}

For the complete per-builtin reference — signatures, parameters, returns, errors, examples — the ProcessResult fields, and the configuration accessors, see docs/API.md.

Starlark API at a glance

Top-level builtins (load("cmd", …)):

  • run(command, cwd?, env?, stdin?, timeout?, combine_output?, realtime_output?, capture_output?) — run an allowlisted command via argv; returns a ProcessResult.
  • which(command) — return the full path of an executable on PATH, or None; never executes.

run returns a ProcessResult struct with fields success, exit_code, stdout, stderr, output, error, pid, start_time, end_time, and duration.

See docs/API.md for the full signatures, return values, errors, and examples of both builtins above.

Configuration

The module's behavioral defaults (cwd, env, timeout, combine_output, realtime_output, capture_output) are configured via environment variables (CMD_*) or per-option get_<key> / set_<key> accessor builtins, and serve as defaults for run. The enable flag and allowlist are not config — they are host-only and cannot be widened from a script. See the Configuration section of docs/API.md for the full option table, defaults, and accessors.

License

MIT

Documentation

Overview

Package cmd provides a Starlark module for executing external commands.

Security posture (PKG-09): the module is DISABLED by default — a script that loads it cannot run anything until the Go host explicitly enables it with an allowlist via NewModuleWithAllow. When enabled it is still deny-by-default: only commands whose canonical form matches an allowlist prefix run. Commands are always executed via argv (exec.Command), never through a shell, so there is no shell interpolation. Command text is hardened against control and zero-width/format characters before allowlist matching and execution. The enable flag and allowlist are host policy set in Go; a script (or an environment variable) can never widen them.

Example

Example shows the happy path: the host enables the module with an allowlist, and a permitted command runs via argv (no shell).

package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/1set/starlet"
	"github.com/starpkg/cmd"
	"go.starlark.net/starlark"
)

// runScript loads the given cmd module into a starlet machine, runs the script,
// and returns its captured print output plus any run error.
func runScript(module *cmd.Module, script string) (string, error) {
	env := starlet.NewDefault()
	env.SetScriptContent([]byte(script))

	var printOutput strings.Builder
	env.SetPrintFunc(func(_ *starlark.Thread, msg string) {
		printOutput.WriteString(msg)
		printOutput.WriteString("\n")
	})

	env.SetLazyloadModules(map[string]starlet.ModuleLoader{"cmd": module.LoadModule()})
	_, err := env.Run()
	return printOutput.String(), err
}

func main() {
	// Enable the module and permit only the "go" tool.
	module := cmd.NewModuleWithAllow("go")

	script := `
load("cmd", "run")

def main():
    result = run("go version")
    print("succeeded:", result.success)
    print("exit code:", result.exit_code)

main()
`
	out, err := runScript(module, script)
	if err != nil {
		log.Fatalf("Failed to run script: %v", err)
	}
	fmt.Print(out)

}
Output:
succeeded: True
exit code: 0

Index

Examples

Constants

View Source
const ModuleName = "cmd"

ModuleName defines the expected name for this module when used in Starlark's load() function

Variables

This section is empty.

Functions

This section is empty.

Types

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module wraps the ConfigurableModule with specific functionality for command execution. enabled, allow and allowAll are host policy (set in Go) and are never overridable by a script or by environment variables.

Example (Disabled)

ExampleModule_disabled shows the secure default: a module created with NewModule is disabled, so run() refuses until the host opts in.

package main

import (
	"fmt"
	"strings"

	"github.com/1set/starlet"
	"github.com/starpkg/cmd"
	"go.starlark.net/starlark"
)

// runScript loads the given cmd module into a starlet machine, runs the script,
// and returns its captured print output plus any run error.
func runScript(module *cmd.Module, script string) (string, error) {
	env := starlet.NewDefault()
	env.SetScriptContent([]byte(script))

	var printOutput strings.Builder
	env.SetPrintFunc(func(_ *starlark.Thread, msg string) {
		printOutput.WriteString(msg)
		printOutput.WriteString("\n")
	})

	env.SetLazyloadModules(map[string]starlet.ModuleLoader{"cmd": module.LoadModule()})
	_, err := env.Run()
	return printOutput.String(), err
}

func main() {
	module := cmd.NewModule() // disabled by default

	script := `
load("cmd", "run")
run("go version")
`
	_, err := runScript(module, script)
	fmt.Println("disabled module refuses to run:", err != nil)

}
Output:
disabled module refuses to run: true

func NewModule

func NewModule() *Module

NewModule creates a new instance of Module with default configurations. The module is DISABLED: run() returns an error until the host enables it with an allowlist via NewModuleWithAllow.

func NewModuleWithAllow

func NewModuleWithAllow(allow ...string) *Module

NewModuleWithAllow returns a module that is ENABLED with the given allowlist. Each entry is a prefix matched against the canonical command (argv joined by a single space) at a word boundary: "git" permits "git status" but not "gitleaks"; "go test" permits "go test ./..." but not "go build". An empty allowlist enables the module but permits nothing (deny-all).

func NewModuleWithAllowAll added in v0.1.1

func NewModuleWithAllowAll() *Module

NewModuleWithAllowAll returns a module that is ENABLED and permits EVERY command — the allowlist check is bypassed entirely. This is the explicit "dangerous, run anything" escape hatch for a host that has already decided the caller is fully trusted (e.g. a CLI operator who passed a --dangerously-allow-all style flag); prefer NewModuleWithAllow with a specific allowlist whenever the set of commands is known. It still applies the same input hardening as every other path (sanitizeCommand rejects control / zero-width characters, and execution stays argv-only — never a shell). Like enable and allow, the allow-all decision is Go-host state bound at construction: nothing a script does, and no environment variable, can set or widen it.

func NewModuleWithConfig

func NewModuleWithConfig(cwd string, env map[string]string, timeout float64, combineOutput, realtimeOutput, captureOutput bool) *Module

NewModuleWithConfig creates a new instance of Module with the given configuration values. Like NewModule, the returned module is DISABLED until enabled with an allowlist.

func (*Module) LoadModule

func (m *Module) LoadModule() starlet.ModuleLoader

LoadModule returns the Starlark module loader with command-specific functions

type ProcessResult

type ProcessResult struct {
	Success   bool
	ExitCode  int
	Stdout    string
	Stderr    string
	Output    string
	Error     string
	PID       int
	StartTime time.Time
	EndTime   time.Time
	Duration  time.Duration
}

ProcessResult represents the result of command execution

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL