Skip to content
Go to console
Go to console

The command line

A stack is deploying, 2 stacks are pending: the gate is open, one crate goes through it and two wait upstreamA stack is deploying, 2 stacks are pending: the gate is open, one crate goes through it and two wait upstream

sluiceway is a command you run on your own machine. It does two kinds of things:

  • init and check read the files of your clone and nothing else: no credentials, no token, no network. Start with init explains them.
  • login, status, stack, preview, tick, rescan and settings talk to the Sluiceway app at console.sluiceway.dev with a personal token you make there. They let you, or a coding agent you give the token to, see your stacks, read a stack’s preview, tick, ask for a scan and change settings from a terminal.

scan, resolve, apply and settle run only in the workflow: they need the run’s identity and the workflow token, and the command line stops at them with a sentence.

Install it

With Node 22 or newer, run it without installing:

Terminal window
npx sluiceway --help

Or bunx sluiceway with Bun. To keep it on your path:

Terminal window
npm i -g sluiceway

The npm package sluiceway is released with the action, from the same tag and with the same version. npx sluiceway@<version> runs the one of a release you reviewed.

A binary, with no Node

Every release also carries the command line as one file per platform, with no Node needed:

Platform File
Linux, x64 sluiceway-linux-x64
Linux, arm64 sluiceway-linux-arm64
macOS, Intel sluiceway-darwin-x64
macOS, Apple silicon sluiceway-darwin-arm64
Windows, x64 sluiceway-windows-x64.exe

Download it from the releases, or from the newest one by its fixed address, and check it against SHA256SUMS from the same release:

Terminal window
curl -fsSLO https://github.com/sluiceway/sluiceway/releases/latest/download/sluiceway-linux-x64
curl -fsSLO https://github.com/sluiceway/sluiceway/releases/latest/download/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
chmod +x sluiceway-linux-x64
sudo mv sluiceway-linux-x64 /usr/local/bin/sluiceway

On macOS, use shasum -a 256 --check --ignore-missing SHA256SUMS. The macOS binaries are not signed: a file downloaded with a browser is stopped by Gatekeeper until you run xattr -d com.apple.quarantine sluiceway-darwin-arm64. A file downloaded with curl is not.

Sign in to the app

Make a token in the app on your own page, Tokens (https://console.sluiceway.dev/settings/tokens). A token is for one org and works for 30, 90 or 365 days, and the app shows it once. Then:

Terminal window
sluiceway login

It asks for the token and does not show what you type. A script pipes it in instead: sluiceway login < token.txt. The token is never read from an environment variable.

login checks the token with the app and keeps it only when it works. A token starts with sluiceway_; anything else, such as a GitHub token pasted by mistake, is refused before it is sent anywhere. It is kept in:

  • macOS: the keychain, under the service sluiceway.
  • Linux: the Secret Service keyring (GNOME Keyring, KWallet) through secret-tool, when there is one.
  • Otherwise, and on Windows: sluiceway/tokens.json under your config directory ($XDG_CONFIG_HOME or ~/.config, and %APPDATA% on Windows), readable by you alone.

The app used to answer at app.sluiceway.dev. A token you kept for that address still signs you in, and the next sluiceway login keeps it under console.sluiceway.dev and takes the old entry out. --app https://app.sluiceway.dev still works while the old address answers.

sluiceway logout takes the token out of every place it is kept. It still works until it expires: revoke it on the Tokens page to stop it at once.

Everything you do with the token counts as you, by the same rules as the app’s pages and the dashboard: a tick is judged by the repo’s tick rule, and the app’s audit log says it came “via the command line”.

The commands

A repo is its name, infra, or org/infra when the org is the token’s. A stack is its stack id, such as apps/api:prod.

Command What it does
sluiceway status The org’s stacks by state, grouped as the app’s org view groups them: Needs you (pending, drifted, preview failed), In flight (deploying and queued), In sync.
sluiceway status <repo> The same for one repo, with its last scan and its dashboard.
sluiceway stack <repo> <stack id> The stack’s row: its state, the counts in the dashboard’s words, what it deletes or replaces, drift, the preview page on GitHub, the run of a deploy and the last deploy.
sluiceway preview <repo> <stack id> Every change of the stack’s preview: what a deploy would create, update, replace or delete, with each resource’s type and name and every property path whole, the values your repo’s dashboard.showValues shows, the drift, and the policies’ failures and warnings. It is the stack’s preview page on GitHub, read through the app, with its link and when the scan wrote it.
sluiceway tick <repo> <stack id> Ticks the stack, as you. A stack that deletes or replaces anything needs --yes.
sluiceway rescan <repo> Asks GitHub for a full scan, as Rescan in the app does.
sluiceway settings <repo> The keys of sluiceway.yaml a settings pull request may change, with the values the file gives them.
sluiceway settings <repo> set <key>=<value> ... Opens a pull request that sets them, and prints its link. A person merges it.

Reading a stack’s preview

preview prints what the stack’s preview page on GitHub holds, the page its row links, in the words of the dashboard’s details but with every path whole:

apps/api:prod in acme/infra: 1 update, 1 replace
Preview page: https://github.com/acme/infra/runs/48213301, of 0a1b2c3, written at 2026-09-26T08:00:00.000Z
Changes
REPLACE aws:rds/instance:Instance main · forced by engineVersion · also changes tags.team
update aws:lambda/function:Function api · memorySize 128 → 256, timeout nothing → 30
create + import aws:sqs/queue:Queue jobs
Outside the code
changed aws:ec2/securityGroup:SecurityGroup web · ingress[0].cidrBlocks[0]

The app reads the page from GitHub when you ask, hands it on, and keeps none of it: no resource name, type, property path or value is stored, logged or cached by the app. The page is written by the scan when the workflow has checks: write, and the app reads it with its Checks permission, which each org accepts on GitHub. A stack with nothing waiting and no drift has no preview, and ends as not found (4); an org that has not accepted the permission yet ends as failed (1), and says so. A page GitHub cut at its size limit says how many changes are not on it; the scan’s job log lists every one.

What a tick does, and what it does not

tick reads the row first, and stops when the stack deletes or replaces anything unless you add --yes. Then it asks the app, which judges the tick by the same rule a tick on the dashboard gets and opens the deployment record. The command prints the app’s answer, waits until the app shows the record, for at most a minute, and ends there:

The tick of network:prod is asked: the deployment record is open.
Deployment record 4242: waiting to start.
The workflow deploys it through a fresh preview and the hash check, and the dashboard says how it went: https://github.com/acme/infra/issues/7

It does not wait for the deploy, and it never says a deploy went out: your workflow deploys the stack on your own runner, and the dashboard row says how it ended. Follow it there, or with sluiceway stack.

Setting a key

Each change is key=value. A value is read as JSON when it is JSON, and as text when it is not:

Terminal window
sluiceway settings infra set dashboard.redact=true tickers=admin
sluiceway settings infra set 'dashboard.sections=["pending","deploying"]'
sluiceway settings infra set 'stacks[apps/api:prod].deploy=on-merge'
sluiceway settings infra set drift.enabled=null # back to the default

To set text that reads as JSON, such as the title true, quote it as JSON: 'dashboard.title="true"'. The app says which keys a pull request may change, and refuses any other, or a value of the wrong kind, naming each one before anything is written.

Each command, by example

The examples are for an org, acme, with one repo, infra, and four stacks: two pending, one queued and one in sync. Each command is shown with what it prints, and with --json, the app’s answer as it came.

sluiceway login

Asks for the token, checks it with the app, and says who it signs you in as and where it keeps the token.

Terminal window
sluiceway login
Signed in to https://console.sluiceway.dev as alice, for acme, with the token laptop, which works until 2026-12-25T00:00:00Z.
The token is kept in the macOS keychain.
Terminal window
sluiceway login --json < token.txt
{
"app": "https://console.sluiceway.dev",
"login": "alice",
"org": "acme",
"token": {
"name": "laptop",
"expiresAt": "2026-12-25T00:00:00Z"
},
"kept": "the macOS keychain"
}

sluiceway status

The org’s stacks by state, the way the app’s org view groups them. With a repo, the same for that repo, with its last scan and its dashboard.

Terminal window
sluiceway status
acme: 2 pending, 1 deploying, 1 in sync, 4 stacks
Last scan: 0a1b2c3 of acme/infra at 2026-09-26T08:00:00Z
Needs you
acme/infra apps/api:prod pending, deletes or replaces 1 update, 1 replace · from #14 by bob
acme/infra network:prod pending 1 update · from #12 by alice
In flight
acme/infra db:prod queued ticked by carol
In sync
acme/infra site:prod
Terminal window
sluiceway status infra --json
{
"org": {
"login": "acme",
"kind": "organization",
"page": "https://console.sluiceway.dev/acme"
},
"repo": {
"name": "acme/infra",
"page": "https://console.sluiceway.dev/acme/infra",
"dashboard": "https://github.com/acme/infra/issues/7",
"stacks": 4,
"counts": {
"pending": 2,
"deploying": 1,
"drifted": 0,
"failed": 0,
"in-sync": 1
},
"recordWriter": null
},
"scan": {
"sha": "0a1b2c3d4e5f",
"at": "2026-09-26T08:00:00Z",
"run": null
},
"counts": {
"pending": 2,
"deploying": 1,
"drifted": 0,
"failed": 0,
"in-sync": 1
},
"total": 4,
"stacks": [
{
"stack": "site:prod",
"repo": "acme/infra",
"state": "in-sync",
"word": "in sync",
"destroys": false,
"line": "",
"at": null
},
{
"stack": "network:prod",
"repo": "acme/infra",
"state": "pending",
"word": "pending",
"destroys": false,
"line": "1 update · from #12 by alice",
"at": null
},
{
"stack": "apps/api:prod",
"repo": "acme/infra",
"state": "pending",
"word": "pending",
"destroys": true,
"line": "1 update, 1 replace · from #14 by bob",
"at": null
},
{
"stack": "db:prod",
"repo": "acme/infra",
"state": "deploying",
"word": "queued",
"destroys": false,
"line": "ticked by carol",
"at": null
}
]
}

sluiceway stack

One stack’s row: its state, the counts in the dashboard’s words, what it deletes or replaces, drift, the preview page on GitHub, the run of a deploy and the last deploy. A line with nothing to say is left out.

Terminal window
sluiceway stack infra network:prod
network:prod in acme/infra: pending
1 update · from #12 by alice
Changes: 1 update
Preview: sluiceway / network:prod, on https://github.com/acme/infra/commit/0a1b2c3d/checks
Dashboard: https://github.com/acme/infra/issues/7
Terminal window
sluiceway stack infra network:prod --json
{
"stack": "network:prod",
"repo": "acme/infra",
"state": "pending",
"word": "pending",
"line": "1 update · from #12 by alice",
"counts": {
"creates": 0,
"updates": 1,
"replaces": 0,
"deletes": 0,
"tracking": 0,
"changed": 0,
"gone": 0
},
"changes": "1 update",
"destroys": null,
"drift": null,
"ticked": false,
"failed": false,
"dashboard": "https://github.com/acme/infra/issues/7",
"preview": {
"name": "sluiceway / network:prod",
"url": "https://github.com/acme/infra/commit/0a1b2c3d/checks"
},
"run": null,
"lastDeploy": null,
"at": "2026-09-26T08:00:00Z"
}

sluiceway preview

Every change of the stack’s preview, read from its preview page on GitHub through the app: the policies, the changes with each path whole and the values dashboard.showValues shows, and the drift. A destroy is in capitals. With --json, the app’s answer as it came.

Terminal window
sluiceway preview infra apps/api:prod
apps/api:prod in acme/infra: 1 update, 1 replace
Preview page: https://github.com/acme/infra/runs/48213301, of 0a1b2c3, written at 2026-09-26T08:00:00.000Z
Policies
warning tags · the queue has no team tag
Changes
REPLACE aws:rds/instance:Instance main · forced by engineVersion · also changes tags.team
update aws:lambda/function:Function api · memorySize 128 → 256, timeout nothing → 30
create + import aws:sqs/queue:Queue jobs
Outside the code
changed aws:ec2/securityGroup:SecurityGroup web · ingress[0].cidrBlocks[0]
Terminal window
sluiceway preview infra apps/api:prod --json
{
"stack": "apps/api:prod",
"repo": "acme/infra",
"page": {
"name": "sluiceway / apps/api:prod",
"url": "https://github.com/acme/infra/runs/48213301",
"sha": "0a1b2c3d4e5f60718293a4b5c6d7e8f901234567",
"at": "2026-09-26T08:00:00.000Z"
},
"title": "apps/api:prod: 1 update, 1 replace",
"policies": [
{
"result": "warning",
"namespace": "tags",
"message": "the queue has no team tag"
}
],
"changes": [
{
"action": "replace",
"tracking": null,
"type": "aws:rds/instance:Instance",
"name": "main",
"properties": [
"engineVersion",
"tags.team"
],
"forcedBy": [
"engineVersion"
],
"values": []
},
{
"action": "update",
"tracking": null,
"type": "aws:lambda/function:Function",
"name": "api",
"properties": [
"memorySize",
"timeout"
],
"forcedBy": [],
"values": [
{
"path": "memorySize",
"old": "128",
"new": "256"
},
{
"path": "timeout",
"old": null,
"new": "30"
}
]
},
{
"action": "create",
"tracking": "import",
"type": "aws:sqs/queue:Queue",
"name": "jobs",
"properties": [],
"forcedBy": [],
"values": []
}
],
"drift": [
{
"action": "changed",
"type": "aws:ec2/securityGroup:SecurityGroup",
"name": "web",
"properties": [
"ingress[0].cidrBlocks[0]"
]
}
],
"unlisted": 0,
"unread": 0
}

sluiceway tick

Ticks the stack as you and stops once the app shows the deployment record. With --json the answer is two of the app’s: tick, what the tick came to, and deploy, the record as the app showed it. The audit log’s line for it says via the command line.

Terminal window
sluiceway tick infra network:prod
The tick of network:prod is asked: the deployment record is open.
Deployment record 4242: waiting to start.
The workflow deploys it through a fresh preview and the hash check, and the dashboard says how it went: https://github.com/acme/infra/issues/7
Terminal window
sluiceway tick infra network:prod --json
{
"tick": {
"outcome": "asked",
"sentence": "The tick of network:prod is asked: the deployment record is open.",
"deployment": {
"id": 4242,
"status": "https://console.sluiceway.dev/api/v1/orgs/acme/repos/infra/deployments/4242"
},
"dashboard": "https://github.com/acme/infra/issues/7"
},
"deploy": {
"deployment": 4242,
"stack": "network:prod",
"repo": "acme/infra",
"who": "ticked by alice via the command line",
"via": "via the command line",
"result": "waiting to start",
"state": "deploying",
"sha": "0a1b2c3d4e5f",
"run": "https://github.com/acme/infra/actions/runs/99",
"at": "2026-09-26T08:05:00Z",
"flagged": false,
"approvedBy": null
}
}
Terminal window
sluiceway tick infra apps/api:prod --json
# exit 5
{
"error": "apps/api:prod deletes or replaces resources (1 replace). Run the tick again with --yes to deploy that.",
"code": "needs-yes",
"exit": 5
}

sluiceway rescan

Asks GitHub for a full scan of the repo, as Rescan in the app does. It deploys nothing.

Terminal window
sluiceway rescan infra
GitHub was asked for a full scan of acme/infra.
Dashboard: https://github.com/acme/infra/issues/7
Terminal window
sluiceway rescan infra --json
{
"repo": "acme/infra",
"outcome": "asked",
"sentence": "GitHub was asked for a full scan of acme/infra.",
"dashboard": "https://github.com/acme/infra/issues/7"
}

sluiceway settings

Without set, the keys a settings pull request may change and the values the file gives them. With set, the pull request it opened. A person merges it.

Terminal window
sluiceway settings infra
sluiceway.yaml of acme/infra at 0a1b2c3: https://github.com/acme/infra/blob/0a1b2c3d4e5f/sluiceway.yaml
dashboard.redact = false
tickers = "write"
drift.enabled = false
Terminal window
sluiceway settings infra --json
{
"repo": "acme/infra",
"file": "sluiceway.yaml",
"sha": "0a1b2c3d4e5f",
"url": "https://github.com/acme/infra/blob/0a1b2c3d4e5f/sluiceway.yaml",
"text": "dashboard:\n redact: false\n",
"problem": null,
"keys": {
"dashboard.redact": false,
"tickers": "write",
"drift.enabled": false
},
"pullRequest": {
"title": "Sluiceway: dashboard settings",
"branch": "sluiceway/dashboard",
"base": "main"
}
}
Terminal window
sluiceway settings infra set dashboard.redact=true
Opened pull request #31 on sluiceway.yaml.
dashboard.redact: false → true
https://github.com/acme/infra/pull/31
Terminal window
sluiceway settings infra set dashboard.redact=true --json
{
"outcome": "opened",
"pullRequest": 31,
"url": "https://github.com/acme/infra/pull/31",
"sentence": "Opened pull request #31 on sluiceway.yaml.",
"changes": [
"dashboard.redact: false → true"
],
"problems": []
}

sluiceway logout

Takes the token out of every place it is kept, and asks the app nothing. With --json, removed names those places.

Terminal window
sluiceway logout
Signed out of https://console.sluiceway.dev: the token is gone from the macOS keychain.
It still works until it expires. Revoke it on https://console.sluiceway.dev/settings/tokens to stop it now.

When it fails

Without --json the reason goes to stderr. With --json it is one document on stdout, and its exit is the exit code. The table below says what each one means.

Terminal window
sluiceway stack infra nope:prod --json
# exit 4
{
"error": "Not found.",
"code": "not-found",
"exit": 4
}
Terminal window
sluiceway status --json
# exit 3
{
"error": "Not signed in to https://console.sluiceway.dev. Make a token on https://console.sluiceway.dev/settings/tokens, then run sluiceway login.",
"code": "not-signed-in",
"exit": 3
}

For agents and scripts

Every command that talks to the app takes --json. It prints one JSON document on stdout: the app’s answer as it came, for a tick { "tick": ..., "deploy": ... }, and on a failure { "error", "code", "exit" }. The answers are the app’s API, version 1 (1.1.0 or later for preview), described at https://console.sluiceway.dev/api/v1/openapi.json.

The exit code says how it ended:

Code Meaning What to do
0 Done
1 Failed: the app could not be reached, a tick or pull request failed, a deployment record failed, the app may not read a preview page yet Read the words, or the dashboard
2 Not understood: the command line, or a token that is not one Fix the command
3 Not signed in, or the token does not work (revoked, expired, the person left the org, the app left it) sluiceway login with a new token
4 Not found, or not in the token’s org, or a stack with no preview Check the repo and the stack id
5 Refused: the tick rule, a tick that happens on GitHub, changes the app refused, a pull request already open, a destroy without --yes Read the reason; do not retry as it is
6 Try again later: the rate limit (the message says in how many seconds), GitHub did not answer, the record not shown yet Wait and run it again

The app allows 120 reads and 10 writes a minute per token.

Where it connects

init and check make no network call. The app’s commands make HTTPS calls to the app alone, with the token you gave, and never to GitHub: the app does what GitHub needs, as you, by its own rules. The token goes nowhere else, and a redirect is refused. --app <address> names another app than https://console.sluiceway.dev, such as one for a test; it must be https, or http to this machine only. The one other process the command line starts is the keychain’s own command, security or secret-tool, which gets the token on stdin.