Security
run_command_line runs arbitrary shell commands. The runtime applies two layers of guardrails before each call: a structural check that blocks dangerous shell constructs, and an optional allowed-tools allowlist declared per-skill.
These guardrails reduce the blast radius if the LLM goes off-script. They are not a sandbox. For real isolation, run with script_runtime: container (Container runtime).
Both checks run by default on every run_command_line call. They can be disabled for local debugging by setting SKILLET_BYPASS_SECURITY=1, which skips the structural check and the allowed-tools allowlist (the 30 s timeout and cwd = skill folder still apply). Leave it unset in production — it exists only to unblock local testing.
Source: src/script_runner/script_runner_security.ts.
Structural check
Every command goes through ScriptRunnerSecurity.checkCommandSecurity first. It rejects:
| Pattern | Why |
|---|---|
&& || | ; | Command chaining. One call, one command. |
$() | Command substitution. |
Backticks ` | Command substitution (legacy form). |
| Newlines | Multi-line commands. |
.. in any token | Path traversal. |
| Absolute paths outside the skill folder | Cross-skill or system access. |
If the LLM emits any of these, the call throws before the shell runs. The LLM gets the error message back and typically tries something simpler on the next turn.
Two practical implications:
- No pipelines. If you need to pipe, put the pipeline inside a script in
scripts/and have the skill run the script as one command. - No
cd.cwdis always the skill folder. Skills that need to operate elsewhere should pass paths relative to the skill folder.
allowed-tools
A skill can further restrict its own commands via allowed-tools in SKILL.md frontmatter:
---
name: git-stats
description: Report git statistics on the current repo.
allowed-tools: Bash(git:*)
---Each pattern has the form Bash(<command>:*). The runtime checks that the command line starts with <command> and rejects everything else. Multiple patterns are space-separated:
allowed-tools: Bash(git:*) Bash(jq:*)Means the skill can call git ... or jq ... but nothing else.
What’s not implemented
- Non-
Bashtools. Only theBash(<command>:*)form is supported. ARead,Write, or bare-Bashtoken inallowed-toolsis not silently ignored —checkAllowedToolsthrowsUnsupported tool: <name>. Keepallowed-toolstoBash(...)patterns. - Argument-level restrictions.
Bash(git:*)lets the LLM callgit push --forceif it wants to. The*is wildcard for arguments; there’s no per-argument filter. - Environment scrubbing. Skills inherit the parent process env (including
OPENAI_API_KEY). Don’t put secrets in your shell env if you don’t want your skills to see them.
Recommended defaults
- Set
allowed-toolson every skill that doesn’t need general shell. A todo-list skill should declareBash(node:*)orBash(npx:*)and call it a day. - Run in container when running untrusted skillets or skillets that touch the network. The structural check +
allowed-toolsare defense-in-depth, not a security boundary. - Don’t bind-mount your home directory into the container. The default mounts only the agent folder; keep it that way unless you have a specific reason otherwise.
Reporting
If you find a way around either guardrail, please open a security issue on the repository.