flyctl
Portable Fly.io operations wrapper. Invoke as /flyctl <subcommand>.
When to Use
This skill is explicit-invocation only (disable-model-invocation: true).
The operator must type /flyctl <subcommand> directly; the model must not
auto-route phrases like "tail fly logs" or "fly deploy" to this side-effectful
skill.
Use /flyctl from a Fly.io app directory containing fly.toml, or pass
-a <app> to override auto-discovery.
Out of Scope
This skill does not perform rollback or cross-provider release reporting.
- For rollback to the previous release, use
/rollback-prod.
- For deployed git-SHA reporting across Vercel/Fly/AWS, use
/deploy-status.
- For app provisioning (
fly launch, region setup), use the Fly UI or your IaC layer.
- For any subcommand not listed in the table or reference files, run
flyctl <subcommand> --help for canonical reference. Read-only verbs (list,
show, status, info, history, version) may proceed without confirmation;
mutating verbs (create, destroy, delete, scale, restart, update, set, unset,
deploy, suspend, resume, regions add/remove, and any verb whose --help text
mentions "destructive", "irreversible", or "will redeploy") MUST be presented
to the operator with the exact command and require explicit y/N approval before
executing.
Auto-Discovery
Every subcommand begins by resolving the CLI binary and the target app. The
operator may override the app with -a <app>; otherwise the skill reads the
root fly.toml in the current working directory. Multi-app monorepos require
the operator to cd into the per-app directory first — this skill refuses to
guess across nested fly.toml files.
# CLI: prefer flyctl, fall back to fly
FLY="$(command -v flyctl || command -v fly || true)"
if [ -z "$FLY" ]; then
echo "flyctl/fly not on PATH; see https://fly.io/docs/flyctl/install/" >&2
exit 2
fi
# App: -a flag wins; otherwise parse root fly.toml only.
# Mirrors parseFlyApp() in deploy-ops.mjs — supports leading whitespace
# around the key, optional whitespace around '=', and BOTH double- and
# single-quoted values.
APP="${ARG_APP:-}"
if [ -z "$APP" ]; then
if [ ! -f fly.toml ]; then
echo "no fly.toml in $(pwd); pass -a <app> or cd into the app directory" >&2
exit 2
fi
APP="$(sed -nE "s/^[[:space:]]*app[[:space:]]*=[[:space:]]*['\"]([^'\"]+)['\"].*/\1/p" fly.toml | head -n 1)"
fi
[ -z "$APP" ] && { echo "could not resolve app name from fly.toml; pass -a <app>" >&2; exit 2; }
# Auth: existing CLI session only — never prompt
"$FLY" auth whoami >/dev/null 2>&1 \
|| { echo "not authenticated; run: $FLY auth login" >&2; exit 2; }
Subcommands
| Sub |
Args |
Invocation |
Confirm? |
Reference |
status |
[-a $APP] |
flyctl status -a $APP |
no |
– |
logs |
[-a $APP] [-i ID] [--region R] |
flyctl logs -a $APP $EXTRA |
no |
logs.md |
releases |
[-a $APP] [--image] |
flyctl releases -a $APP --json |
no |
releases.md |
deploy |
[-a $APP] [--image REF] [--strategy S] |
flyctl deploy -a $APP --image $REF |
YES if prod |
deploy.md |
secrets list |
[-a $APP] |
flyctl secrets list -a $APP |
no |
secrets.md |
secrets set |
[-a $APP] [--stage] K=V... |
flyctl secrets set -a $APP $KV |
YES if prod |
secrets.md |
secrets unset |
[-a $APP] K... |
flyctl secrets unset -a $APP $K |
YES if prod |
secrets.md |
machines list |
[-a $APP] |
flyctl machines list -a $APP --json |
no |
machines.md |
machines restart <id> |
[-a $APP] |
flyctl machines restart $ID -a $APP |
no |
machines.md |
machines stop <id> |
[-a $APP] |
flyctl machines stop $ID -a $APP |
YES if prod |
machines.md |
machines start <id> |
[-a $APP] |
flyctl machines start $ID -a $APP |
no |
machines.md |
machines destroy <id> |
[-a $APP] |
flyctl machines destroy $ID --force -a $APP |
YES if prod |
machines.md |
scale show |
[-a $APP] |
flyctl scale show -a $APP |
no |
scale.md |
scale count <n> |
[-a $APP] |
flyctl scale count $N -a $APP |
YES if prod or n=0 |
scale.md |
scale vm <preset> |
[-a $APP] [--memory MB] |
flyctl scale vm $P -a $APP |
YES if prod |
scale.md |
scale memory <mb> |
[-a $APP] |
flyctl scale memory $MB -a $APP |
YES if prod |
scale.md |
ssh console |
[-a $APP] [-s ID] |
flyctl ssh console -a $APP |
no (interactive) |
ssh.md |
ssh console -C <cmd> |
[-a $APP] [-s ID] |
flyctl ssh console -C "$CMD" -a $APP |
YES if prod |
ssh.md |
ssh sftp |
[-a $APP] |
flyctl ssh sftp shell -a $APP |
no |
ssh.md |
proxy |
<local:remote> [-a $APP] |
flyctl proxy $P -a $APP |
no |
proxy.md |
health |
[-a $APP] [--path P] |
discover endpoints from fly.toml, proxy + curl |
no |
health.md |
| any other |
– |
run flyctl <cmd> --help, classify the verb, stop and prompt for explicit operator approval before executing any mutating verb |
per-op |
– |
Confirmation Contract
Destructive ops on prod-flavored apps require the operator to type the exact app
name. The heuristic for "prod-flavored" is: app name does NOT match
*-staging*, *-preview*, *-pr-*, *-dev*, or *-test*.
Heuristic is best-effort. App names that contain these substrings but
are nonetheless prod (e.g. my-developer-portal, payments-test-of-record)
will silently skip the confirmation gate. Until v1.1 ships confirm_always
config, use --no-confirm deliberately for known non-prod apps with unusual
names, and add a comment in your runbook for prod apps that would false-match.
# no_confirm_flag MUST be derived ONLY from an explicitly parsed --no-confirm
# flag in $@. The skill never reads any environment variable (e.g. NO_CONFIRM)
# as a bypass — operator intent must be on the command line for each invocation.
confirm_if_prod() {
local app="$1" op="$2" no_confirm_flag="$3"
[ "$no_confirm_flag" = "1" ] && return 0
case "$app" in
*-staging*|*-preview*|*-pr-*|*-dev*|*-test*) return 0 ;;
esac
printf 'About to run %s on PROD app %s.\nType the app name to confirm: ' "$op" "$app" >&2
read -r reply < /dev/tty || { echo "no interactive TTY; pass --no-confirm explicitly or run interactively" >&2; exit 2; }
[ "$reply" = "$app" ] || { echo "confirmation failed" >&2; exit 1; }
}
# Argument-parsing stub the skill MUST follow before calling confirm_if_prod:
no_confirm_flag=0
parsed=()
for arg in "$@"; do
case "$arg" in
--no-confirm) no_confirm_flag=1 ;;
*) parsed+=("$arg") ;;
esac
done
set -- "${parsed[@]}"
Lighter than /rollback-prod's ROLLBACK PROD literal because deploy/scale are
less catastrophic than rollback; typing the app name is harder to muscle-memory
than a fixed string and is context-aware. Confirmation bypass derives from the
parsed CLI flag only — env vars cannot grant it.
Rules
- Auto-discover
app from root fly.toml; never hardcode names, regions, VM presets, or secret names.
- Resolve CLI as
flyctl first, fly second; never re-install.
- Use existing CLI auth (
flyctl auth whoami); never prompt for tokens.
- Destructive ops on prod-flavored apps require typed confirmation matching the app name.
--no-confirm is honored only when passed explicitly as a deliberate operator flag; never via env, templates, or instructions that may reach automated invocations.
- Print the exact
flyctl ... command before executing (audit trail — terminal scrollback only, not durable).
- Preserve flyctl native exit codes; remap only discovery/auth failures to
2.
- Multi-app monorepos: refuse to guess — operator must
cd into the app dir or pass -a <app>.
- Never echo secret values to chat — only names.
- For unlisted subcommands: run
flyctl <subcommand> --help first, classify mutability from the verb and the help text, and require explicit operator y/N approval before executing any mutating op. Read-only verbs (list, show, status, info, history, version) may run without prompting.
Failure Modes
| Condition |
Exit |
Behavior |
| Success |
0 |
preserve flyctl exit |
| Confirmation declined / flyctl exit 1 |
1 |
preserve |
No fly.toml in cwd and no -a |
2 |
print: "no fly.toml in <pwd>; pass -a <app> or cd into the app directory" |
flyctl/fly not on PATH |
2 |
print install URL |
auth whoami fails |
2 |
print flyctl auth login |
fly.toml exists but app line missing |
2 |
print: "could not resolve app name from fly.toml; pass -a <app>" |
Invalid --image ref (pre-flight regex) |
2 |
print expected format |
Optional Config
This file is NOT consumed by v1.0.0. It documents the v1.1 config surface
so contributors can preview it. Operators whose prod apps don't match
*-staging* / *-preview* / *-pr-* / *-dev* / *-test* will get
typed-confirmation prompts on every mutating op until v1.1 wires confirm_never
to override the heuristic; pass --no-confirm per-invocation until then.
See examples/fly-targets.example.json.
Planned v1.1 config schema (not yet wired — for preview only):
| Key |
Type |
Semantics |
confirm_always |
string[] |
App names that always require typed confirmation, regardless of the prod-name heuristic. |
confirm_never |
string[] |
App names that never require typed confirmation (overrides heuristic for known non-prod apps). |
deploy.strategy |
string |
Default --strategy value for flyctl deploy (e.g. bluegreen). Overridable per-invocation. |
deploy.require_digest |
boolean |
If true, reject --image refs without a @sha256: digest suffix (enforce immutable deploys). |
When v1.1 wires this file, it will be read via jq from .claude/flyctl.json at skill startup.
See also
/deploy-status — read-only SHA / release status across all configured deploy targets (use this when you want a snapshot, not an action).
/rollback-prod — confirmation-gated rollback across all targets (use this for incident response; /flyctl is for non-incident, fly-only ops).
1---2name: flyctl3description: Explicit-invocation Fly.io operations wrapper. Run `/flyctl <subcommand>` to deploy (with --image), tail logs, manage secrets, inspect machines, scale, SSH, proxy, check health, list releases, and show status. Auto-discovers the app from fly.toml in the current directory. Side-effectful ops on prod-flavored apps require typed confirmation matching the app name. Rollback is delegated to /rollback-prod; cross-provider SHA reporting is delegated to /deploy-status. Unlisted subcommands require `flyctl <subcommand> --help` first.4---56# flyctl78Portable Fly.io operations wrapper. Invoke as `/flyctl <subcommand>`.910## When to Use1112This skill is **explicit-invocation only** (`disable-model-invocation: true`).13The operator must type `/flyctl <subcommand>` directly; the model must not14auto-route phrases like "tail fly logs" or "fly deploy" to this side-effectful15skill.1617Use `/flyctl` from a Fly.io app directory containing `fly.toml`, or pass18`-a <app>` to override auto-discovery.1920## Out of Scope2122This skill does **not** perform rollback or cross-provider release reporting.2324- For rollback to the previous release, use `/rollback-prod`.25- For deployed git-SHA reporting across Vercel/Fly/AWS, use `/deploy-status`.26- For app provisioning (`fly launch`, region setup), use the Fly UI or your IaC layer.27- For any subcommand not listed in the table or reference files, run28 `flyctl <subcommand> --help` for canonical reference. Read-only verbs (list,29 show, status, info, history, version) may proceed without confirmation;30 mutating verbs (create, destroy, delete, scale, restart, update, set, unset,31 deploy, suspend, resume, regions add/remove, and any verb whose `--help` text32 mentions "destructive", "irreversible", or "will redeploy") MUST be presented33 to the operator with the exact command and require explicit y/N approval before34 executing.3536## Auto-Discovery3738Every subcommand begins by resolving the CLI binary and the target app. The39operator may override the app with `-a <app>`; otherwise the skill reads the40root `fly.toml` in the current working directory. Multi-app monorepos require41the operator to `cd` into the per-app directory first — this skill refuses to42guess across nested `fly.toml` files.4344```bash45# CLI: prefer flyctl, fall back to fly46FLY="$(command -v flyctl || command -v fly || true)"47if [ -z "$FLY" ]; then48 echo "flyctl/fly not on PATH; see https://fly.io/docs/flyctl/install/" >&249 exit 250fi5152# App: -a flag wins; otherwise parse root fly.toml only.53# Mirrors parseFlyApp() in deploy-ops.mjs — supports leading whitespace54# around the key, optional whitespace around '=', and BOTH double- and55# single-quoted values.56APP="${ARG_APP:-}"57if [ -z "$APP" ]; then58 if [ ! -f fly.toml ]; then59 echo "no fly.toml in $(pwd); pass -a <app> or cd into the app directory" >&260 exit 261 fi62 APP="$(sed -nE "s/^[[:space:]]*app[[:space:]]*=[[:space:]]*['\"]([^'\"]+)['\"].*/\1/p" fly.toml | head -n 1)"63fi64[ -z "$APP" ] && { echo "could not resolve app name from fly.toml; pass -a <app>" >&2; exit 2; }6566# Auth: existing CLI session only — never prompt67"$FLY" auth whoami >/dev/null 2>&1 \68 || { echo "not authenticated; run: $FLY auth login" >&2; exit 2; }69```7071## Subcommands7273| Sub | Args | Invocation | Confirm? | Reference |74| ----------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------- |75| `status` | `[-a $APP]` | `flyctl status -a $APP` | no | – |76| `logs` | `[-a $APP] [-i ID] [--region R]` | `flyctl logs -a $APP $EXTRA` | no | [`logs.md`](references/logs.md) |77| `releases` | `[-a $APP] [--image]` | `flyctl releases -a $APP --json` | no | [`releases.md`](references/releases.md) |78| `deploy` | `[-a $APP] [--image REF] [--strategy S]` | `flyctl deploy -a $APP --image $REF` | **YES if prod** | [`deploy.md`](references/deploy.md) |79| `secrets list` | `[-a $APP]` | `flyctl secrets list -a $APP` | no | [`secrets.md`](references/secrets.md) |80| `secrets set` | `[-a $APP] [--stage] K=V...` | `flyctl secrets set -a $APP $KV` | **YES if prod** | [`secrets.md`](references/secrets.md) |81| `secrets unset` | `[-a $APP] K...` | `flyctl secrets unset -a $APP $K` | **YES if prod** | [`secrets.md`](references/secrets.md) |82| `machines list` | `[-a $APP]` | `flyctl machines list -a $APP --json` | no | [`machines.md`](references/machines.md) |83| `machines restart <id>` | `[-a $APP]` | `flyctl machines restart $ID -a $APP` | no | [`machines.md`](references/machines.md) |84| `machines stop <id>` | `[-a $APP]` | `flyctl machines stop $ID -a $APP` | **YES if prod** | [`machines.md`](references/machines.md) |85| `machines start <id>` | `[-a $APP]` | `flyctl machines start $ID -a $APP` | no | [`machines.md`](references/machines.md) |86| `machines destroy <id>` | `[-a $APP]` | `flyctl machines destroy $ID --force -a $APP` | **YES if prod** | [`machines.md`](references/machines.md) |87| `scale show` | `[-a $APP]` | `flyctl scale show -a $APP` | no | [`scale.md`](references/scale.md) |88| `scale count <n>` | `[-a $APP]` | `flyctl scale count $N -a $APP` | **YES if prod or n=0** | [`scale.md`](references/scale.md) |89| `scale vm <preset>` | `[-a $APP] [--memory MB]` | `flyctl scale vm $P -a $APP` | **YES if prod** | [`scale.md`](references/scale.md) |90| `scale memory <mb>` | `[-a $APP]` | `flyctl scale memory $MB -a $APP` | **YES if prod** | [`scale.md`](references/scale.md) |91| `ssh console` | `[-a $APP] [-s ID]` | `flyctl ssh console -a $APP` | no (interactive) | [`ssh.md`](references/ssh.md) |92| `ssh console -C <cmd>` | `[-a $APP] [-s ID]` | `flyctl ssh console -C "$CMD" -a $APP` | **YES if prod** | [`ssh.md`](references/ssh.md) |93| `ssh sftp` | `[-a $APP]` | `flyctl ssh sftp shell -a $APP` | no | [`ssh.md`](references/ssh.md) |94| `proxy` | `<local:remote> [-a $APP]` | `flyctl proxy $P -a $APP` | no | [`proxy.md`](references/proxy.md) |95| `health` | `[-a $APP] [--path P]` | discover endpoints from `fly.toml`, proxy + curl | no | [`health.md`](references/health.md) |96| _any other_ | – | run `flyctl <cmd> --help`, classify the verb, **stop and prompt for explicit operator approval before executing any mutating verb** | per-op | – |9798## Confirmation Contract99100Destructive ops on prod-flavored apps require the operator to type the exact app101name. The heuristic for "prod-flavored" is: app name does NOT match102`*-staging*`, `*-preview*`, `*-pr-*`, `*-dev*`, or `*-test*`.103104> **Heuristic is best-effort.** App names that _contain_ these substrings but105> are nonetheless prod (e.g. `my-developer-portal`, `payments-test-of-record`)106> will silently skip the confirmation gate. Until v1.1 ships `confirm_always`107> config, use `--no-confirm` deliberately for known non-prod apps with unusual108> names, and add a comment in your runbook for prod apps that would false-match.109110```bash111# no_confirm_flag MUST be derived ONLY from an explicitly parsed --no-confirm112# flag in $@. The skill never reads any environment variable (e.g. NO_CONFIRM)113# as a bypass — operator intent must be on the command line for each invocation.114confirm_if_prod() {115 local app="$1" op="$2" no_confirm_flag="$3"116 [ "$no_confirm_flag" = "1" ] && return 0117 case "$app" in118 *-staging*|*-preview*|*-pr-*|*-dev*|*-test*) return 0 ;;119 esac120 printf 'About to run %s on PROD app %s.\nType the app name to confirm: ' "$op" "$app" >&2121 read -r reply < /dev/tty || { echo "no interactive TTY; pass --no-confirm explicitly or run interactively" >&2; exit 2; }122 [ "$reply" = "$app" ] || { echo "confirmation failed" >&2; exit 1; }123}124125# Argument-parsing stub the skill MUST follow before calling confirm_if_prod:126no_confirm_flag=0127parsed=()128for arg in "$@"; do129 case "$arg" in130 --no-confirm) no_confirm_flag=1 ;;131 *) parsed+=("$arg") ;;132 esac133done134set -- "${parsed[@]}"135```136137Lighter than `/rollback-prod`'s `ROLLBACK PROD` literal because deploy/scale are138less catastrophic than rollback; typing the app name is harder to muscle-memory139than a fixed string and is context-aware. Confirmation bypass derives from the140parsed CLI flag only — env vars cannot grant it.141142## Rules143144- Auto-discover `app` from root `fly.toml`; never hardcode names, regions, VM presets, or secret names.145- Resolve CLI as `flyctl` first, `fly` second; never re-install.146- Use existing CLI auth (`flyctl auth whoami`); never prompt for tokens.147- Destructive ops on prod-flavored apps require typed confirmation matching the app name.148- `--no-confirm` is honored only when passed explicitly as a deliberate operator flag; never via env, templates, or instructions that may reach automated invocations.149- Print the exact `flyctl ...` command before executing (audit trail — terminal scrollback only, not durable).150- Preserve flyctl native exit codes; remap only discovery/auth failures to `2`.151- Multi-app monorepos: refuse to guess — operator must `cd` into the app dir or pass `-a <app>`.152- Never echo secret values to chat — only names.153- For unlisted subcommands: run `flyctl <subcommand> --help` first, classify mutability from the verb and the help text, and **require explicit operator y/N approval before executing any mutating op**. Read-only verbs (`list`, `show`, `status`, `info`, `history`, `version`) may run without prompting.154155## Failure Modes156157| Condition | Exit | Behavior |158| ---------------------------------------- | ---- | ----------------------------------------------------------------------------- |159| Success | `0` | preserve flyctl exit |160| Confirmation declined / flyctl exit 1 | `1` | preserve |161| No `fly.toml` in cwd and no `-a` | `2` | print: "no fly.toml in `<pwd>`; pass -a `<app>` or cd into the app directory" |162| `flyctl`/`fly` not on PATH | `2` | print install URL |163| `auth whoami` fails | `2` | print `flyctl auth login` |164| `fly.toml` exists but `app` line missing | `2` | print: "could not resolve app name from fly.toml; pass -a `<app>`" |165| Invalid `--image` ref (pre-flight regex) | `2` | print expected format |166167## Optional Config168169**This file is NOT consumed by v1.0.0.** It documents the v1.1 config surface170so contributors can preview it. Operators whose prod apps don't match171`*-staging*` / `*-preview*` / `*-pr-*` / `*-dev*` / `*-test*` will get172typed-confirmation prompts on every mutating op until v1.1 wires `confirm_never`173to override the heuristic; pass `--no-confirm` per-invocation until then.174175See [`examples/fly-targets.example.json`](examples/fly-targets.example.json).176177Planned v1.1 config schema (not yet wired — for preview only):178179| Key | Type | Semantics |180| ----------------------- | ---------- | ------------------------------------------------------------------------------------------------ |181| `confirm_always` | `string[]` | App names that always require typed confirmation, regardless of the prod-name heuristic. |182| `confirm_never` | `string[]` | App names that never require typed confirmation (overrides heuristic for known non-prod apps). |183| `deploy.strategy` | `string` | Default `--strategy` value for `flyctl deploy` (e.g. `bluegreen`). Overridable per-invocation. |184| `deploy.require_digest` | `boolean` | If `true`, reject `--image` refs without a `@sha256:` digest suffix (enforce immutable deploys). |185186When v1.1 wires this file, it will be read via `jq` from `.claude/flyctl.json` at skill startup.187188## See also189190- [`/deploy-status`](../deploy-status/SKILL.md) — read-only SHA / release status across all configured deploy targets (use this when you want a snapshot, not an action).191- [`/rollback-prod`](../rollback-prod/SKILL.md) — confirmation-gated rollback across all targets (use this for incident response; `/flyctl` is for non-incident, fly-only ops).