Run a command inside a small, explicit profile.
Use runseal when a repository needs explicit local resources, named wrappers,
and env/argv/symlink setup without becoming a task runner, secret manager, or
deployment orchestrator.
It is designed for repos that carry private local paths such as .local,
kubeconfig files, SSH config, tool directories, or wrapper scripts, and need
one command to expose those paths to child tools predictably.
Declare profile-local resources:
[resources]
root = ".local"
[[injections]]
type = "env"
[injections.vars]
APP_SSH_CONFIG = "resource://ssh/config"
APP_SECRET_DIR = "resource://secrets"Inspect what runseal resolved:
runseal @profile
runseal @resources
runseal @resolve resource://ssh/configRun an external command or a named wrapper inside the profile:
runseal bash -lc 'echo "$APP_SSH_CONFIG"'
runseal :ssh host --run ./probe.shRunseal inspection commands are read-only and do not run profile injections:
runseal @profile
runseal @resources
runseal @resolve resource:// resource://ssh/config
runseal @transpile --input-lang=seal --output-lang=bash ./operator.seal
runseal @tool json get '{"releaseVersion":"v0.6.0"}' '.releaseVersion'
runseal @wrappers
runseal @which :sshThese commands answer the first debugging questions: which profile was selected,
where resources resolve, which wrappers are visible, and which wrapper file a
:name command will execute.
Command routing is based on the first command token:
runseal <cmd>runs an external command inside the profile.runseal :<cmd>runs a profile wrapper.runseal @<cmd>runs a runseal-owned command.
For example:
runseal --profile ./runseal.toml bash -lc 'echo "$RUNSEAL_PROFILE_PATH"'Use runseal profile without @ to run an external command named profile.
Fits well:
- repo-local private resources
- explicit one-command execution environments
- named wrappers with discoverability
- passing profile-scoped paths to tools like
ssh,kubectl,uv, orterraform
Not trying to be:
- a task dependency graph
- a secret lifecycle manager
- a deployment orchestrator
- a shell auto-activation tool
runseal currently supports three profile capabilities plus explicit wrapper
and internal command namespaces:
env: export environment variables and ordered env operations.symlink: create symlinks for the command lifecycle, then clean them up.argv: inject fixed arguments after a matching child command token.resource://...: resolve profile-local resource paths inside env values.
Unix:
curl -fsSL https://runseal.perish.uk/manage.sh | shWindows:
irm https://runseal.perish.uk/manage.ps1 | pwshInstall a beta or one explicit version:
curl -fsSL https://runseal.perish.uk/manage.sh | sh -s -- install --channel beta
curl -fsSL https://runseal.perish.uk/manage.sh | sh -s -- install --version v0.1.0-beta.10Uninstall:
curl -fsSL https://runseal.perish.uk/manage.sh | sh -s -- uninstallIf --profile is omitted, profile discovery walks from the current directory
to filesystem root. At each directory, format priority is:
runseal.tomlrunseal.yamlrunseal.ymlrunseal.json
If no ancestor profile is found, discovery falls back to:
$RUNSEAL_PROFILE_HOME/default.toml$RUNSEAL_PROFILE_HOME/default.yaml$RUNSEAL_PROFILE_HOME/default.yml$RUNSEAL_PROFILE_HOME/default.json
RUNSEAL_HOME is the runseal configuration root. When unset it defaults to ~/.runseal.
RUNSEAL_PROFILE_HOME is the profile directory. When unset it defaults to $RUNSEAL_HOME/profiles.
Each child command receives:
RUNSEAL_HOMERUNSEAL_PROFILE_HOMERUNSEAL_PROFILE_PATHRUNSEAL_WRAPPER_PATH
[resources]
root = ".local"
[[injections]]
type = "env"
[injections.vars]
RUNSEAL_ENV = "dev"
LOCAL_ROOT = "resource://"
SSH_CONFIG = "resource://ssh/config"
[[injections]]
type = "env"
[[injections.ops]]
op = "prepend"
key = "PATH"
value = "./bin"
separator = "os"
dedup = true
[[injections]]
type = "symlink"
source = "./tool"
target = "./.runseal-bin/tool"
on_exist = "replace"
cleanup = true
[[injections]]
type = "argv"
command = "ssh"
args = ["-F", ".local/ssh/config"]Lifecycle symlink targets are single-owner. Do not run concurrent runseal
invocations that manage the same target with cleanup = true; one process can
replace or remove the link while another still expects to own it. Use distinct
targets when commands need to run in parallel under the same profile.
resource://path/to/file is a profile-only path literal. A profile that uses
resource URIs must declare:
[resources]
root = ".local"The resource root may be relative to the profile directory, absolute, or ~
expanded. In env injection values, runseal rewrites resource URIs to absolute
paths under that configured root. For example, with root = ".local",
resource://ssh/config resolves to <profile-dir>/.local/ssh/config.
resource:// and resource://. resolve to the resource root itself.
Child commands receive only the resolved absolute path. They do not receive
or need to understand resource://.
Resource URIs are resolved only when the env value is exactly the URI. runseal does not perform partial string interpolation inside env values.
Resource paths must be relative URI-style paths. Empty paths, empty path
segments, ., .., backslash separators, and : inside path segments are
rejected. Resource paths are resolved without checking whether the file exists.
If the command token starts with :, runseal resolves it as a wrapper
executable instead of a literal program name:
runseal :ssh host --run ./probe.sh -- argWrapper lookup order is:
<profile-dir>/.runseal/wrappers/<name>.seal<profile-dir>/.runseal/wrappers/<name>.sh$RUNSEAL_HOME/wrappers/<name>.seal$RUNSEAL_HOME/wrappers/<name>.sh
The profile directory is the directory containing RUNSEAL_PROFILE_PATH.
Successful profile and wrapper paths are normalized absolute paths.
The child working directory is not changed. A resolved wrapper receives:
RUNSEAL_WRAPPER_NAMERUNSEAL_WRAPPER_FILE
Seal wrappers use the .seal suffix and are interpreted directly by runseal.
On Unix, shell wrappers use the .sh suffix and must be executable. On Windows,
runseal also checks .exe, .cmd, and .bat when the wrapper name has no
extension.
.seal files are bash-runnable wrapper glue. They are meant for
cross-platform repository operations where the bash/PowerShell shared shape is
clear. The boundary is syntax shape, not script size:
- ordinary command execution, assignment, functions,
if,while,case,shift,"$@", command success predicates such asif git checkout "$branch"; then, and command-scoped env overlays such asKUBECONFIG="$kubeconfig" kubectl "$@" - bash
[ ... ]tests for ordinary predicates - explicit
runseal @tool ...calls for atomic glue where bash and PowerShell do not share a clean expression
Use .seal as the profile integration layer: it should pass caller-specific
paths, env names, and defaults explicitly. Keep reusable domain atoms in
@tool, such as SSH config inspection, stdin script execution, path-list
joining, branch slugging, Gitee PR API calls, and encrypted local archive
round trips. For example, a wrapper can expose :ssh <host> --run <script>
while runseal @tool ssh script run owns the stdin, argv forwarding, and host
config details.
For example:
fail() {
printf '%s\n' "$1" >&2
exit 1
}
if [ -z "$channel" ]; then
fail "channel missing"
fi
raw=$(gh run list --json databaseId)
run_id=$(runseal @tool json get "$raw" '.[0].databaseId')Runseal interprets .seal wrappers directly when called as runseal :name.
Use runseal @transpile --input-lang=seal --output-lang=bash <file> or
--output-lang=powershell to inspect generated targets.
Seal is not intended to become a general scripting language. If a workflow wants richer parsing, data structures, or platform-specific behavior, move that part to Python, Ruby, JavaScript, etc. and call it from the wrapper.
If the command token starts with @, runseal resolves it as a runseal internal
command instead of a literal program name:
runseal @profile
runseal @resources
runseal @resolve resource:// resource://ssh/config
runseal @transpile --input-lang=seal --output-lang=sealir ./operator.seal
runseal @wrappers
runseal @which :sshRunseal-owned commands do not run profile injections. Inspection commands are
read-only; @tool is the explicit atomic tool runtime.
@profileprints the resolved runseal runtime paths. If resources are configured, it also printsRUNSEAL_RESOURCE_ROOT.@resourcesprints the resolved resource root.@resolve resource://...prints resolved absolute resource paths, one per argument.@transpile --input-lang=<lang> --output-lang=<lang> <source>transpiles explicit glue languages and prints the generated output. Cold start supportsbash,seal,powershell, andsealirinputs and outputs for the currently recognized intersection.@tool <namespace> <command> ...runs an atomic runseal tool command. Cold start supports JSON, string, regex, integer, process, filesystem, archive, SSH config, GitHub, Gitee, and Cloudflare helpers. Runrunseal @tool --helpfor the complete tool index. Tools are reusable atoms: they may read generic defaults such as service tokens, but profile-specific paths and env names should be supplied by the calling wrapper.@wrapperslists the effective wrappers visible to the current profile.@which :<name>prints the wrapper file that:<name>resolves to.
YAML and JSON profiles use the same structure:
resources:
root: .local
injections:
- type: env
vars:
LOCAL_ROOT: resource://
SSH_CONFIG: resource://ssh/config{
"resources": {
"root": ".local"
},
"injections": [
{
"type": "env",
"vars": {
"LOCAL_ROOT": "resource://",
"SSH_CONFIG": "resource://ssh/config"
}
}
]
}Initialize local development hooks:
runseal :initcargo fmt --check
cargo testRepo-local operator commands use runseal itself:
runseal :cloudflare manage-inspect
runseal :pr --dry-run
runseal :release --channel beta --dry-run