The ubiquitous ./go script β as a Go binary.
uGo is a CLI tool that executes project-specific commands defined in YAML configuration.
It follows the ThoughtWorks ./go script pattern: provide a common vocabulary for each
project, with verbs that are context-aware based on the current working directory.
The binary name is flexible β rename it and the help text and config file name follow automatically, allowing multiple instances for different projects.
go install github.com/PyratLabs/ugo@latestOr build from source:
go build -o ugo .Create <binary-name>.yaml in your project root:
tools:
ansible-playbook:
min_version: "2.10.0"
version_cmd: "ansible-playbook --version"
download_url: "https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html"
go:
min_version: "1.25.0"
version_cmd: "go version"
kubectl:
download_url: "https://kubernetes.io/docs/tasks/tools/"
commands:
plan:
cmd: ansible-playbook --check environments/${environment}/inventory.yaml playbooks/${playbook}.yaml
description: "Run an ansible playbook in check mode."
arguments:
- name: environment
match: "environments/*/inventory.yaml"
- name: playbook
match: "playbooks/*.yaml"
lint:
cmd: go test ./...
description: "Run test suite"
deploy:
cmd: kubectl apply -f ./deployments/${service} --context ${region}
description: "Deploy a service"
arguments:
- name: service
match: "[a-z][a-z0-9-]+"
- name: region
values: [us-east-1, eu-west-1, ap-southeast-1]ugo plan dev ensure-ssh # runs: ansible-playbook --check environments/dev/inventory.yaml playbooks/ensure-ssh.yaml
ugo lint # runs: go test ./...
ugo check # verify tool dependencies
ugo version # print the version
ugo plan --help # shows argument validation rules
ugo --no-color # disable colored outputuGo loads configuration from two locations and merges them (local overrides global):
| Scope | Path |
|---|---|
| Global | ~/.config/<binary>/config.yaml |
| Local | <project-root>/<binary>.yaml |
Set shell_options to prepend shell flags to all commands that run via sh -c (every cmd β single-line and multiline β and all cmds items):
shell_options: "set -euo pipefail"This enables strict mode: exit on error (-e), unset variable error (-u), and pipe failure detection (-o pipefail).
tools:
<binary>:
min_version: "<semver>" # optional minimum version
max_version: "<semver>" # optional maximum version
version_cmd: "<cmd>" # how to get the version string (defaults to "<binary> --version")
download_url: "<url>" # shown if tool is missing (optional)If no min_version or max_version is specified, uGo only checks that the tool exists in $PATH:
tools:
terraform: # just check it exists
download_url: "https://terraform.io/downloads"commands:
<verb>:
cmd: "<single command with ${arg} templates>" # string: runs via "sh -c", so quoting, pipes, and && work
cmds: # list: each item runs via sh -c (supports shell features)
- "echo ${arg}"
- |
echo "multi-line"
echo "script"
env: # optional: environment variables (supports ${arg} / ${prompt} expansion)
MY_VAR: "value"
TOKEN: "${api_token}"
description: "<short help text>"
arguments: # positional argument definitions (optional)
- name: <arg1>
values: [val1, val2] # optional: restrict to enum values
match: "<glob or regex>" # optional: validate with file glob or regex
prompts: # interactive prompts (optional)
- name: api_token
description: "Enter your API token"
sensitive: true # optional: masks input and displayPrompts collect user input interactively at runtime. Values are referenced in cmd/cmds/env via ${name} templates, just like arguments.
commands:
deploy:
cmd: kubectl apply -f ${manifest} --token ${api_token}
description: "Deploy with an API token"
arguments:
- name: manifest
match: "*.yaml"
prompts:
- name: api_token
description: "Enter API token"
sensitive: true| Field | Description |
|---|---|
name |
Template variable name (used as ${name} in commands) |
description |
Question shown to the user at the prompt |
sensitive |
If true, input is hidden during entry and displayed as ******** in command output; the real value is passed to the command |
from_env_var |
If set, the prompt is skipped when this environment variable is already set; its value is used directly. Falls through to interactive prompt when unset or empty |
Running the above command will prompt for the token, mask it on screen, and expand both ${manifest} and ${api_token} in the command:
$ ugo deploy deployment.yaml
Enter API token: # input is hidden
π deploy: kubectl apply -f deployment.yaml --token ********With from_env_var, if the environment variable is set, no prompt appears:
$ API_TOKEN=sk-abc123 ugo deploy deployment.yaml
π deploy: kubectl apply -f deployment.yaml --token ********The help output shows the env var fallback:
Prompts:
api_token Enter API token (or $API_TOKEN) (sensitive)env values support ${name} template expansion from both arguments and prompts:
commands:
deploy:
cmds:
- 'echo "$MY_TOKEN"'
env:
MY_TOKEN: "${api_token}"
prompts:
- name: api_token
description: "Enter API token"
sensitive: trueArguments support three validation modes:
| Mode | Config | Behavior |
|---|---|---|
| Enum | values: [dev, staging, prod] |
Value must be in the list |
| Glob | match: "playbooks/*.yaml" |
Checks files on disk; accepts full path, basename, basename without extension, or directory name |
| Regex | match: "[a-z]+" |
Full-string regex match (auto-anchored) |
| Exclude | exclude: [default] |
Disallowed values (hidden from help output) |
Glob vs regex is auto-detected: if the pattern contains * or ? it's treated as a file glob.
Every cmd runs via sh -c, so shell features β quoting, embedded whitespace, pipes (|), and operators (&&, ||, redirects) β behave as written:
commands:
plan:
cmd: terraform workspace select "${workspace}" && terraform plan
description: "Select a workspace and plan"cmd with multiline β runs the entire block as a shell script via sh -c:
commands:
plan:
cmd: |
terraform workspace select -or-create=true "${workspace}"
terraform plan
description: "Run a terraform plan"cmds list β each item runs via sh -c, so shell features (variables, subshells, pipes) work:
commands:
deploy:
cmds:
- 'echo "Deploying to $ENV"'
- |
echo "Running in subshell"
(
cd /tmp && pwd
)
env:
ENV: "production"
description: "Deploy with shell features"Combine match with exclude to hide certain values from glob matches:
commands:
plan:
cmd: terraform workspace select "${workspace}" && terraform plan
arguments:
- name: workspace
match: config/*.yaml
exclude: [default]Excluded values are rejected during validation and hidden from help output.
# config: cmd: ansible-playbook environments/${environment}/inventory.yaml
# args: environment with match "environments/*/inventory.yaml"
ugo plan dev β ansible-playbook environments/dev/inventory.yaml
ugo plan test β β argument 'environment': no file matching pattern "environments/*/inventory.yaml" found for value "test"# config: cmd: kubectl apply -f ./deployments/${service} --context ${region}
# args: region with values [us-east-1, eu-west-1, ap-southeast-1]
ugo deploy api us-east-1 β kubectl apply -f ./deployments/api --context us-east-1
ugo deploy api us-west-2 β β argument 'region': value "us-west-2" not in allowed values: [us-east-1, eu-west-1, ap-southeast-1]Running a verb without required arguments shows the error followed by argument and prompt validation rules:
$ ugo plan
β expected 2 argument(s) for 'plan': environment, playbook
Arguments:
environment dev, staging, prod
playbook ensure-ssh, setup-db
Usage:
ugo plan <environment> <playbook> [flags]When prompts are defined, they appear in the help output:
$ ugo deploy --help
Arguments:
manifest *.yaml
Prompts:
api_token Enter API token (sensitive)
Usage:
ugo deploy <manifest> [flags]Before executing any verb, uGo checks configured tools:
- Verifies each binary exists in
$PATH - Extracts version from
version_cmdoutput - Compares against
min_versionandmax_versionusing semver
Run ugo check manually to inspect all tool status.
$ ugo check
β
ansible-playbook (v2.20.5)
β
docker (v28.5.1)
β
All tool dependencies satisfied
$ ugo check
β nonexistent-tool is not installed, download at: https://example.com
β
docker (v28.5.1)
β Tool checks faileduGo's job is to run commands you have configured, so the configuration and the argument values you pass are trusted inputs. A few properties are worth understanding:
uGo reads ./<binary>.yaml from the current directory and merges it over your global config. Running a verb β or ugo check β executes the version_cmd of each configured tool found on your $PATH, and runs the verb's cmd/cmds via sh -c. Changing into a directory and running a verb therefore runs that directory's configuration, the same way make, npm run, or a ./go script would.
To guard against running an unfamiliar repository's config, uGo gates the local config behind a trust prompt (see Trusting a directory). The global config (~/.config/<binary>/config.yaml) is user-owned and always trusted. ugo --help and ugo version never execute anything from the config, so they are safe to run anywhere.
The first time you run a verb (or ugo check) in a directory with a local config, uGo asks before executing anything:
$ ugo build
β οΈ /home/me/project/ugo.yaml is not trusted.
Running a verb here will execute the commands defined in this file.
Trust it? [y/N]:Answering y records the config as trusted and runs it; anything else aborts without executing. Trust is content-addressed: uGo stores the path together with a SHA-256 of the file's contents in ~/.config/<binary>/trust.json. If the config is later edited (e.g. a git pull changes it), trust is automatically revoked and you are prompted again.
For non-interactive use (CI/CD), pass --trust to skip the prompt and record the config as trusted:
ugo --trust buildWhen no terminal is attached and --trust is not given, uGo refuses to run rather than executing an untrusted config silently. To revoke trust, delete the relevant entry (or the whole file) from ~/.config/<binary>/trust.json.
${name} placeholders are substituted into the command string before it is handed to sh -c, and the values are not shell-quoted. A value like foo; rm -rf ~ placed in an unconstrained argument runs as written.
If a verb can receive values from an untrusted or external source (CI variables, webhooks, etc.), constrain those arguments:
values: [...]β accept only an explicit set (exact match), ormatch: "<regex-or-glob>"β validate against a fully-anchored regex, or an on-disk glob.
Arguments with neither are accepted verbatim. Unresolved ${name} placeholders (a typo, or a deliberate $HOME) are passed through and expanded by the shell.
sensitive: true masks a prompt's value in uGo's own output only β the π line shows ********. The real value is still expanded into the command, which means:
- it is passed to
sh -c, so it can be visible in the process list (ps,/proc) to other local users while the command runs; and - enabling shell tracing through
shell_options(e.g.set -x) echoes the expanded command β including the secret β to stderr.
Prefer passing secrets through the environment β reference a ${prompt} from an env: value and use "$VAR" in the command, rather than interpolating the secret directly into cmd β and avoid set -x when handling sensitive prompts.
uGo uses UTF-8 icons and colors for status output. Use --no-color to disable:
ugo --no-color plan dev ensure-sshXan Manning, 2020