Sip is a command-line tool that simplifies managing Dart and Flutter projects. It helps you run scripts, manage pub commands, execute tests, and more — all from a single configuration file.
-
Define and run scripts from a
scripts.yamlfile- Supports nested scripts
- Run scripts concurrently
-
Run pub commands (
pub get,pub upgrade, etc.)- Runs recursively and concurrently
-
Run Dart/Flutter tests
- Recursive mode
- Fail fast mode (stops running tests after the first failure)
- Run only Dart or only Flutter tests
-
Customize executable commands (
dart,flutter, etc.) -
Validate
scripts.yamlbefore running it (sip validate) -
Work with AI coding assistants
--jsonoutput forlist,run --print,testandvalidate- An MCP server (
sip mcp) - Reference files for the popular assistants (
sip ai)
dart pub global activate sip_clisip --helpCreate a scripts.yaml file in your project root:
# scripts.yaml
hello:
world: echo "Hello, World!"Run your script:
sip run hello worldThe scripts.yaml file defines all scripts and configuration for Sip. It usually lives in your project root.
Sip uses dart and flutter by default. To override them:
(executables):
dart: fvm dart
flutter: fvm flutterA script maps a key to a command:
build_runner: dart run build_runner buildsip run build_runnerCommands can also be lists:
build_runner:
- cd packages/core && dart run build_runner build
- cd packages/data && dart run build_runner build- Allowed pattern:
^_?([a-z][a-z0-9_.\-]*)?(?<=[a-z0-9_])$ - Keys wrapped in parentheses (e.g.,
(command)) are reserved - Must start with a letter or
_ - Must end with a letter, number, or
_
You can nest scripts:
format:
ui: cd packages/ui && dart format .
core: cd packages/core && dart format .Use (command) to specify a default command for the top level script itself:
format:
(command): dart format .
ui: cd packages/ui && dart format .
core: cd packages/core && dart format .sip list # or sip lsSearch:
sip list build_runnerTo explore nested scripts, you can use the --help flag:
sip run build_runner --help--json prints every script as data instead of a tree — its dotted key, its
aliases and description, the raw commands as written, the fully resolved
commands that will actually run, and the scripts.yaml line it was declared
on:
sip list --json
sip list build --json # only scripts matching a query
sip list --json --no-resolve # skip expanding references and variables{
"version": 1,
"scriptsYaml": "/repo/scripts.yaml",
"executables": { "dart": "fvm dart" },
"variables": { "projectRoot": "/repo" },
"scripts": [
{
"path": ["build_runner", "build"],
"key": "build_runner.build",
"name": "build",
"parent": "build_runner",
"aliases": ["b"],
"description": null,
"private": false,
"runnable": true,
"bail": false,
"commands": ["${{ build_runner._ }} build"],
"resolved": [
{ "command": "fvm dart run build_runner build", "concurrent": false }
],
"resolveError": null,
"env": null,
"location": { "file": "/repo/scripts.yaml", "line": 28, "column": 3 }
}
]
}Pass the path back to sip run (sip run build_runner build). A script
that cannot be resolved reports why in resolveError instead of failing the
whole listing.
Use ${{ key }} to reference another script:
pub_get: dart pub get
pub_get_ui: cd packages/ui && ${{ pub_get }}References work with nesting:
pub:
(command): dart pub
get: "${{ pub }} get"
ui: cd packages/ui && ${{ pub.get }}Important
Nested references are separated by dots. A colon (${{ pub:get }}) is not
substituted — it is passed to the shell verbatim and fails with
bad substitution.
Sip forwards only the flags and arguments you explicitly include using ${{ --FLAG_NAME }}:
test: dart test ${{ --coverage }}Examples:
sip run test --coverage=coverage
sip run other --flag value1 value2 --verboseUnspecified flags are ignored.
Private keys (starting with _) cannot be run directly, but can be referenced:
format:
_hidden: dart format .
(command): cd packages/ui && ${{ format._hidden }}Use --bail to stop running as soon as a command fails:
sip run format --bailOr set it in config:
format:
(bail): true
(command): dart formatNote
(bail) must be given an explicit true. An empty value ((bail):) is
parsed as null and read as false.
Run scripts concurrently using (+):
format:
(command):
- echo "Running format"
- (+) cd packages/ui && dart format .
- (+) cd packages/core && dart format .
- echo "Finished running format"You can disable concurrency by passing the --no-concurrent flag.
sip run format --no-concurrentSip provides built-in variables:
${{ projectRoot }}: The nearestpubspec.yamlto the current working directory${{ scriptsRoot }}: The nearestscripts.yamlto the current working directory${{ cwd }}: The current working directory${{ dartOrFlutter }}: Eitherdartorflutterexecutable, depending on the nearestpubspec.yamlto the current working directory${{ dart }}: Thedartexecutable${{ flutter }}: Theflutterexecutable
Define custom variables under (variables):
(variables):
ocarinaTune: |-
echo "Playing Song of Time..."Use them:
play: ${{ ocarinaTune }}(variables):
flutter: fvm flutter
build_runner:
build: dart run build_runner build
watch:
(description): Run build_runner in watch mode
(command): dart run build_runner watch
(aliases): [w]
test:
(command): "${{ flutter }} test ${{ --coverage }}"
coverage: "${{ test }} --coverage=coverage"
echo:
dirs:
- echo "${{ projectRoot }}"
- echo "${{ scriptsRoot }}"
- echo "${{ cwd }}"
format:
_command: dart format .
(command):
- echo "Running format"
- (+) ${{ format.ui }}
- (+) ${{ format.data }}
- (+) ${{ format.application }}
- echo "Finished running format"
ui: cd packages/ui && ${{ format._command }}
data: cd packages/data && ${{ format._command }}
application: cd application && ${{ format._command }}Sip always executes from the directory containing your scripts.yaml, regardless of your current working directory.
sip run build_runner buildRun sip run --help for all available flags.
--print shows the resolved commands without executing them, and --json
alongside it prints them as data:
sip run build_runner build --print
sip run build_runner build --print --jsonsip validate checks the file without running anything, and reports each
problem against the line it was written on:
sip validate
sip validate --json # structured diagnostics with stable codes
sip validate --fatal-warnings # exit non-zero for warnings tooscripts.yaml:8:1: error: ${{ a:b }} is not a valid substitution, so it is passed to the shell unchanged.
Separate script names with dots: ${{ a.b }}
scripts.yaml:16:3: warning: (bail) has no value, which sip reads as false.
Write `(bail): true`.
1 error, 1 warningIt finds:
| Code | Severity | What it catches |
|---|---|---|
invalid-yaml |
error | The file does not parse |
unknown-reference |
error | ${{ x }} names no script or variable |
malformed-substitution |
error | ${{ a:b }} and friends, passed to the shell verbatim |
reference-has-no-command |
error | A reference to a group with no (command) |
circular-reference |
error | Scripts that reference each other in a loop |
invalid-key |
error | A script name sip rejects |
empty-bail |
warning | (bail): with no value, which reads as false |
unknown-reserved-key |
warning | (descriptions) and other near-misses |
duplicate-alias |
warning | An alias claimed twice, which deactivates it |
empty-script |
warning | A script with no (command) and no subscripts |
Errors exit 78. Warnings exit 0 unless --fatal-warnings is passed.
Diagnostics go to stderr, so --json output on stdout stands alone.
You can load environment variables before running a script:
build:
(command): flutter build apk
(env): .env # or ['.env', '.env.local']Or run a command to generate env vars:
(env):
file: .env # or ['.env', '.env.local']
command: dart run generate_env.dart # can be a list of commandsOr inline variables:
(env):
vars:
FLUTTER_BUILD_MODE: releaseParent script env overrides nested script env.
Use --never-exit to restart a command whenever it fails:
sip run build_runner watch --never-exitWarning
Use with caution — the command restarts indefinitely.
You can stop the script by pressing Ctrl + C.
There is a 1 second delay between each run of the command, to prevent any runaway scripts.
Run all tests:
sip test --recursiveDart-only:
sip test --dart-onlyFlutter-only:
sip test --flutter-onlyFail fast:
sip test --bailNote
sip test fails when a test fails, when the test process exits non-zero, and
when it finds no packages to test — running nothing is not a pass.
Machine-readable results:
sip test --json{
"passed": false,
"counts": { "passing": 12, "failing": 1, "skipped": 0 },
"failures": [
{
"path": "test/a_test.dart",
"test": "fails loudly",
"error": "Expected: <2>\n Actual: <1>"
}
],
"skipped": [],
"errors": []
}errors holds failures that are not test failures — a compile error, a
crashed runner, output sip could not parse. A run with an empty failures
list and a non-empty errors list still failed, which is why passed
exists rather than leaving it to be inferred from the counts.
Warning
Experimental and strictly opt-in — may change or be removed without notice.
For large Flutter widget-test suites, --experimental-bucket combines test files into a handful of generated bucket files (one flutter test invocation per shard) to cut per-file VM-isolate startup overhead:
sip test --experimental-bucketFiles that can't be safely combined (an explicit non-default TestWidgetsFlutterBinding subtype, or a test-surface mutation like tester.view.physicalSize left unreset) always run in their own isolated wrapper, never sharing an isolate with anything else. If a bucket's combined run looks untrustworthy — a compile error or hard binding assertion cuts it short — just that bucket's files are automatically discarded and re-run individually, and this is logged clearly.
This does not catch arbitrary test-state leakage between files sharing an isolate (e.g. an unreset image cache) — that class of bug can't be caught statically or by the fallback above, so treat a bucketed run as a strong signal, not a guarantee of unbucketed-equivalent results.
For CI matrix jobs, split the generated buckets across N jobs with --bucket-shard-index/--bucket-shard-count (entirely sip-side; distinct from flutter test's own --shard-index/--total-shards, which isn't recommended for this since it still pays full test-graph discovery/compile cost per shard):
sip test --experimental-bucket --bucket-shard-index=0 --bucket-shard-count=4--bucket-count controls how many combined bucket files are generated (default: number of processors).
Install a sip reference file so your AI assistant knows how scripts.yaml and
the CLI work:
sip ai agents # AGENTS.md
sip ai claude # CLAUDE.md
sip ai cursor # .cursor/rules/sip-*.mdc
sip ai copilot # .github/copilot-instructions.md
sip ai windsurf # .windsurfrules
sip ai cline # .clinerules
sip ai all # every file aboveExisting files are left alone; pass --force to overwrite them.
sip mcp runs sip as an MCP server over
stdio, so an assistant discovers your scripts as tools instead of having to
remember a CLI convention:
| Tool | What it does |
|---|---|
list_scripts |
Every script, with the commands it actually runs |
dry_run |
What a script expands to, without running it |
run_script |
Run a declared script; returns exit code, stdout and stderr |
validate |
Check scripts.yaml for problems |
Point your assistant at it:
{
"mcpServers": {
"sip": { "command": "sip", "args": ["mcp"] }
}
}run_script takes a script name, never a command, so it cannot run anything
that is not declared in scripts.yaml. What a declared script does is of
course up to your project, so the tool is marked destructive.
sip's output is meant to be read by whoever is reading it:
- Colour follows the terminal. Piped or redirected output is plain text.
--color/--no-color,NO_COLOR,FORCE_COLORandTERM=dumboverride the guess. - The update notice never touches stdout. It goes to stderr, and is
skipped entirely (along with its network request) when stdout is not a
terminal.
--no-version-checkorSIP_NO_VERSION_CHECK=1also turn it off. --jsonoutput stands alone on stdout. Every message, warning and error goes to stderr.
sip pub getAutomatically detects whether to use dart or flutter.
Recursive:
sip pub get --recursivesip pub upgradeUpgrade all or specific packages:
sip pub upgrade provider shared_preferencessip pub downgradesip pub deps --jsonConstrain versions to your current resolution:
sip pub constrainConstrain only selected packages:
sip pub constrain provider shared_preferences:2.3.0Pin versions:
sip pub constrain provider --pinUnpin:
sip pub constrain provider --no-pinSupported flags:
recursivedev_dependenciesbump(breaking,major,minor,patch)dry-rundart-onlyflutter-onlypinno-pin
