Concurrent SSH remote command runner — run the same shell command on many hosts in parallel, stream per-host output, and get a human-readable summary plus machine-scrapeable results.
# 1. Install
curl -fsSL https://raw.githubusercontent.com/kevin197011/kkfly/main/install.py | python3
# 2. First run generates kkfly.yml — edit hosts + command, then:
kkfly
# 3. Optional: JSON report for automation
kkfly --json
jq -e '.overall == "success"' kkfly.jsonLatest release (Linux / macOS, amd64 / arm64):
# cache-bust if raw.githubusercontent.com serves stale content:
curl -fsSL "https://raw.githubusercontent.com/kevin197011/kkfly/main/install.py?t=$(date +%s)" | python3Pin version or install path:
VERSION=0.1.13 curl -fsSL https://raw.githubusercontent.com/kevin197011/kkfly/main/install.py | python3
curl -fsSL https://raw.githubusercontent.com/kevin197011/kkfly/main/install.py | python3 - --version 0.1.13
INSTALL_DIR=~/.local/bin curl -fsSL https://raw.githubusercontent.com/kevin197011/kkfly/main/install.py | python3| Platform | Install path | Notes |
|---|---|---|
| Linux | /usr/local/bin |
Use sudo when the directory is not writable; installer auto-adds PATH |
| macOS | /usr/local/bin or ~/.local/bin |
Falls back to ~/.local/bin without root |
Artifacts are verified against the release checksums.txt (SHA-256). Requires Python 3.8+ (stdlib only).
Releases: github.com/kevin197011/kkfly/releases
kkfly # default config: ./kkfly.yml
kkfly -config /path/to/config.yml
kkfly --version
kkfly --json # write kkfly.json (no kkfly.log)
kkfly -json-out /tmp/report.json| Flag | Description |
|---|---|
-config PATH |
YAML config (default: kkfly.yml in cwd) |
-json |
Write JSON report to kkfly.json |
-json-out PATH |
Write JSON to this path |
-version, -v |
Print version and exit |
Exit code: 0 only when every host succeeds; non-zero on any host failure, bad config, or JSON write error.
First run: if the config file is missing, kkfly writes a kkfly.yml template, prints its path, and exits.
Copy configs/kkfly.yml or edit the auto-generated kkfly.yml:
user: ubuntu
private_key_path: ~/.ssh/id_ed25519
port: 22
concurrency: 20
sudo: true
connect_timeout_seconds: 10
command_timeout_seconds: 900
max_output_bytes_per_stream: 262144
strict_host_key_checking: false
command: |
echo hello
hosts:
- 1.2.3.4
- example.com| Field | Required | Default | Description |
|---|---|---|---|
user |
yes | — | SSH username |
private_key_path |
yes* | — | Path to SSH private key (~ expanded; relative to config dir) |
private_key_content |
yes* | — | Inline key; takes precedence over private_key_path |
hosts |
yes | — | Hostnames or IPs (non-empty list) |
concurrency |
yes | — | Max parallel hosts (≥ 1, capped at host count) |
command |
yes | — | Remote shell command (bash -lc on remote) |
port |
no | 22 |
SSH port |
sudo |
no | false |
Run via sudo -n bash -lc (needs NOPASSWD on remote) |
connect_timeout_seconds |
no | 10 |
SSH dial timeout |
command_timeout_seconds |
no | 900 |
Per-host command deadline (15 min) |
max_output_bytes_per_stream |
no | 262144 |
Max captured bytes per stdout/stderr stream |
known_hosts_path |
no | ~/.ssh/known_hosts |
Used when strict checking is on |
strict_host_key_checking |
no | true |
true: verify keys, auto-create ~/.ssh/known_hosts, accept-new on first connect; false: skip verification |
disable_stdout_stderr_print |
no | false |
Suppress live out/err lines (capture still applies) |
* One of private_key_path or private_key_content is required. Encrypted keys are not supported (non-interactive by design).
Unless -json / -json-out is set, a plain-text kkfly.log is written alongside terminal output (no ANSI). Log open failures are warned and ignored.
Set NO_COLOR or KKFLY_NO_COLOR to disable terminal colors.
──────────────────────────────────────────────────────────────
kkfly · 2 hosts · ×20 · sudo
2026-07-05 01:30:05
$ curl -fsSL ... | bash (3 lines)
──────────────────────────────────────────────────────────────
No per-line timestamps (time is in the header). Queued hosts are omitted.
HOST STAGE DETAIL
─────────────────────────────────────────────────────────
1.2.3.4 conn
1.2.3.4 run
1.2.3.4 out hello
1.2.3.4 done [2/5] exit 0 · 1.2s
Stages: conn → run → out / err → done.
══════════════════════════════════════════════════════════════
✓ SUCCESS · 2/2 ok · 5.123s
══════════════════════════════════════════════════════════════
errors # only when failures exist
2× dial tcp: i/o timeout
HOST EXIT TIME ERROR
1.2.3.4 0 1.2s
example.com 1 3.4s connection refused
overall: success · partial_failure · failure
Plain footer for pipelines — grep between the markers:
# --- KKFLY_COLLECT ---
overall=success hosts_total=2 hosts_ok=2 hosts_failed=0 duration=5.123s wall_seconds=5.123
failed_hosts_tsv=bad.host other.host
# RESULT_TSV
HOST STATUS EXIT DURATION_MS ERROR
1.2.3.4 OK 0 1200
bad.host FAIL 1 3400 connection refused
# --- END KKFLY_COLLECT ---
-json / -json-out writes a structured report (and skips kkfly.log):
| Top-level | Per host (results[]) |
|---|---|
overall, hosts_total, hosts_ok, hosts_failed, failed_hosts |
host, status, exit_code, started, finished, duration, error, stdout, stderr |
status values: succeeded, failed.
go build -o kkfly ./cmd/kkflyruby push.rb runs the full release pipeline:
go test ./... → git add/commit/pull/push → git tag vX.Y.Z → git push tag
↓
GitHub Actions (GoReleaser) → GitHub Release + binaries
ruby push.rb # auto bump patch (v0.1.16 → v0.1.17)
VERSION=0.2.0 ruby push.rb # pin version
SKIP_RELEASE=1 ruby push.rb # push code only, no tag
SKIP_TEST=1 ruby push.rb # skip go test
KK_GIT_DRY_RUN=1 ruby push.rb # dry-run git stepsIf HEAD already has a v*.*.* tag, the release step is skipped (safe to re-run after a no-op push).
Requires the kk-git gem for conventional commit messages and auto push.
MIT — see LICENSE.