Omahub
← All plugins
E

Kamal Deploy

by Eduard G. Castelló

Discover Kamal deploy.<env>.yml targets under your project folders, check any number across one or many projects, and run setup, deploy, logs, console, accessory, and lock actions against all of them at once. Most actions run in the background with a spinner and a result card; a few needing a real terminal (console/shell, tailing logs, rollback) open one for you. Includes a Provision Wizard that generates a tailored server-setup script and, optionally, config/deploy.yml itself.

Security review

Potentially dangerous behavior detected · 12 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
ae4ecc7
Scanned
3 weeks ago

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
ae4ecc7
Reviewed
3 weeks ago

This is a legitimate Kamal deployment helper. The deterministic scan's high-risk findings come from the provision template (curl|sh, mkswap, systemctl enable), which is only generated on demand and executed by the user against their own servers, not at plugin install time. The plugin's own scripts are defensive (path restrictions, cache safety, input validation) and contain no hidden malicious behavior.

  • The generated provision script runs `curl -fsSL https://get.docker.com | sh` on the target server; this is the official Docker installer but is a remote-code-execution pattern and should be reviewed before use.
  • The provision script makes system-level changes on remote hosts (swap, fstab, sshd, ufw, systemd services) and could lock out SSH if misconfigured; it is user-generated and user-run.
  • The plugin can run powerful `kamal` commands (deploy, rollback, accessory remove) against user-selected targets; these are user-initiated and expected for a deployment tool.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/eddygarcas/omarchy-kamal-deploy --enable
Developer Tools #bar #quickshell

Kamal Deploy

An Omarchy shell plugin that turns every Kamal project on your machine into a bar-panel checklist. It finds config/deploy.yml and config/deploy.<env>.yml files under folders you point it at, and lets you check any number of destinations — across one project or several — then run setup / deploy / logs / console / accessory / lock actions against all of them at once, each one opening (or refocusing) your default terminal so you never have to open one by hand.

Why

Deploying multiple Kamal apps across multiple environments usually means a hand-rolled shell menu (kamal_menu() in .bashrc, a bin/deploy script, whatever) that only knows about the one repo you're sitting in. This plugin generalizes that: point it at your projects folder once, and every destination in every project shows up as a clickable target, from the bar, on any workspace.

Install

git clone https://github.com/eddygarcas/omarchy-kamal-deploy.git \
  ~/.config/omarchy/plugins/eduard.kamal-deploy
omarchy-shell shell rescanPlugins
omarchy plugin enable eduard.kamal-deploy

What it does

Click the bar icon (a compass) to open the panel. A ⬇ update icon appears next to ↻ (both bigger now, next to the panel title) only when this plugin's own GitHub repo has a newer manifest.json version than the one installed — checked once per panel-open, silently, never surfacing an error if you're offline or GitHub's unreachable, it just means no icon. Hover it to see the current/latest versions; click it for update instructions — this plugin doesn't update itself. An earlier version did (git fetch + fast-forward from origin/main), but that pulls in whatever is on the branch right now regardless of whether the marketplace's own commit verification has reviewed it, silently trading the reviewed, listed snapshot for unreviewed code — outside what a plugin's own update mechanism can safely do on its own. Update by hand instead: cd into the plugin's folder, git pull, omarchy restart shell.

  • Search folders — folders to scan for config/deploy*.yml. Type a path (~ is expanded, relative paths are taken as relative to your home folder) — press Tab to autocomplete it, terminal-style: one match completes the folder name and appends / so the next Tab drills in; several matches complete as far as they agree and show the rest as clickable chips — then hit Add, or click Detect to pick up common project folders (~/Code, ~/Projects, ~/dev, ~/RubymineProjects, …) automatically. Saved in shell.json under "eduard.kamal-deploy", so it survives restarts. Click ↻ to rescan on demand. As a safety baseline, every folder must resolve inside your home folder — /etc, /, or a ..-traversal trick like ~/../../etc are all rejected, both here and again by discover.sh itself (in case shell.json was hand-edited).

  • Checklist — one section per project (named from service: in its base deploy.yml, or the folder name, with a small language icon — a Nerd Font glyph for Ruby (any *.rb file or a Gemfile/Rakefile in the project root), Go (*.go or go.mod), TypeScript (*.ts/*.tsx or tsconfig.json), or Node (*.js/*.jsx or package.json), a plain dot for anything else — checked in that order, so Ruby wins if a project has both a Gemfile and a package.json, e.g. a Rails app with a JS asset pipeline), one checkbox per destination (default for config/deploy.yml, <env> for each config/deploy.<env>.yml) — laid out as a two-column grid once a project has more than one destination, one column otherwise. Select all / Clear toggle everything at once. As soon as anything is checked, a SELECTED (N) chip row appears (click a chip's ✕ to uncheck just that one) followed by the shared action bar — every button carries an icon for its action:

    • Deploy — Provision (ruby provision <env> — enabled only once every selected target's project has a provision script, see Provision Wizard below), Setup (kamal setup -v), Deploy (kamal lock release then kamal deploy -v), Rollback (kamal rollback).
    • Application — Tail logs (kamal app logs -f), Rails console (kamal app exec -i 'bin/rails console'), Bash shell (kamal app exec -i bash), Restart (kamal app restart), Details (kamal details).
    • Operations — Lock status, Release lock, Audit log.
    • Accessories — known accessory names (parsed from every selected project's accessories: block) are listed as a hint; type one into the field and Boot / Reboot / Stop / Restart / Logs / Remove become available (Reboot releases the lock first, same as the original script). Letters, numbers, - and _ only — Logs opens a real terminal via a launcher that reassembles its arguments through a shell, so this is enforced strictly rather than just rejecting whitespace.

    Clicking any action runs it once per checked target — a staging + production selection queues (or opens) two of whatever that action uses, one per destination, since each needs its own kamal ... -d <env> in its own project directory; they can't be merged into a single command.

Most actions never open a terminal at all. Provision, Setup, Deploy, Restart, Details, Lock status, Release lock, Audit log, and every accessory action except its own Logs run in the background — a spinner appears under RESULTS while it's in flight, then the real command's output (and exit status) renders in a scrollable console-styled card you can dismiss whenever you're done reading it, or clear in bulk once a batch finishes. Each one also fires a desktop notification (notify-send, app name "Kamal Deploy") the moment it finishes — done or failed — since the panel is often closed by then; it names the project/env/action and, on failure, the exit code, and just points you back to the panel rather than repeating the full output. A handful of actions still open a real terminal, because they either need genuine interactive input or never finish on their own: Tail logs and an accessory's own Logs (-f, streams forever), Rails console and Bash shell (kamal app exec -i, a real interactive session), and Rollback (prompts you to pick a version). Those go through scripts/run.sh, opened via omarchy-launch-or-focus-tui — your actual default terminal (foot, kitty, alacritty, ghostty, whatever xdg-terminal-exec resolves to), with a stable per-target-per-action window so clicking the same action twice on the same target refocuses the running one instead of piling up windows.

Both paths resolve a clicked target the same way, through $XDG_RUNTIME_DIR/eduard.kamal-deploy/targets.json (written by the last scan) — a background run just calls scripts/run-background.sh (no terminal, no colors, no "press any key" pause, plain captured stdout/stderr) instead of scripts/run.sh; both share the actual action→command mapping from scripts/dispatch.sh so the two paths can't drift apart on what a given action actually runs. Both also share scripts/ensure-kamal.sh, which only checks whether kamal is on PATH — it deliberately doesn't try to install it for you (that would mean fetching and running whatever's currently published to RubyGems with no version pin and no confirmation), so a missing kamal fails with a clear error telling you to install it yourself.

Setup and Deploy — the two actions that build and push a Docker image from this machine, unlike every other action which only talks to the remote host's own docker daemon over SSH — first check docker info. If local Docker isn't reachable, the action stops right there (no kamal process ever spawned) and a desktop notification tells you to start Docker and retry, instead of failing a few seconds later on a confusing docker/API error. This lives in dispatch_action itself (action_requires_docker in scripts/dispatch.sh), so it applies no matter which of run.sh/run-background.sh ends up running the action.

Provision Wizard

Click + next to the rescan button (↻) to open the wizard for setting up a brand-new server. It:

  1. Target folder — pick one of your discovered projects, or type a custom path (Tab to autocomplete, same as search folders). Same home-folder safety baseline as search folders — a path outside ~ is flagged in red and blocks both the checks and Generate, and generate-provision.sh refuses to write outside $HOME even if it's called some other way.
  2. Checks — runs scripts/provision-check.sh against that folder and shows: does config/deploy.yml (or deploy.<env>.yml) exist, does the Gemfile have the kamal and net-ssh gems, does ssh-add -l show a loaded key, and whether a provision file is already there (would be overwritten).

Target folder and Checks are shared — below them, Deploy and Provision are two separate tabs, since deploy.yml and the provisioning script are independent artifacts you might only want one of at a time. Deploy is selected by default, since a fresh project needs config/deploy.yml before provisioning even makes sense; switch to Provision freely, your Deploy fields stay put. Styled as classic attached tabs — rounded on top, flat on the bottom — with the active one flush against the card holding its fields below it, and the inactive one a separate closed pill beside it.

  1. Tailor (Provision tab) — swap size, storage path/owner, extra firewall ports beyond 22, Docker log rotation size/count, ulimit, and four toggles (UFW, fail2ban, unattended-upgrades, SSH hardening).
  2. Generate (Provision tab) — renders templates/provision.erb with those choices via scripts/generate-provision.sh (plain Ruby ERB, no gems needed beyond the stdlib) into <target>/provision, chmod +x, then rescans so the project's Provision button lights up immediately.

The template itself is the user's own Scaleway/Ubuntu Kamal provisioning script (idempotent SSH-based steps: essentials, swap, storage dir, Docker + kamal network, then the four toggleable hardening steps, then Docker daemon log rotation), parameterized instead of hardcoded. A toggle turned off removes that step from the generated script entirely rather than leaving it disabled in place — re-run the wizard any time to regenerate with different choices; it always overwrites, never merges.

Deploy config (Deploy tab)

The same wizard can also generate config/deploy.yml itself — the provision script needs it anyway, since it reads servers: from that file at runtime to know which hosts to SSH into. Type an Environment (blank for the base config, or a name like staging/production — letters, numbers, - and _ only, since it becomes part of the generated filename — for a config/deploy.<env>.yml override), Role/Hosts (comma-separated IPs or hostnames) plus an optional Cmd to override the container's default command, a second role like workers is optional too, and click Generate deploy.yml:

  • Base config (blank environment) — only offered when config/deploy.yml doesn't exist yet. Renders service, image, retain_containers, and servers: from the form, then a full skeleton for everything a real deployment typically needs — proxy (SSL, app port, healthcheck, response timeout, request buffering), registry, env (clear/secret), and builder — followed by the exact same aliases/ssh/volumes/asset_path/boot/accessories guidance kamal init ships (copied verbatim from the installed kamal gem's own template, never re-evaluated by this wizard's own ERB pass — so its one illustrative <%= %> example line stays literal, matching what kamal init itself produces). Every value that isn't service/image/servers is either a genuine Kamal default (/up healthcheck, Dockerfile, arch: amd64, retain_containers: 5) or a generic placeholder straight from Kamal's own configuration docs (DB_USER/DB_PASSWORD for env, <your registry server> for registry) — never made-up, and never copied from any one real project's actual values.
  • Named environment — a minimal, servers:-only override, the normal Kamal pattern for adding a destination to an app that already has a base config. If config/deploy.yml doesn't exist yet either, it's generated too (full skeleton, same Service/Image/servers from the form) — an override with nothing to override isn't useful on its own, and this saves a second trip through the wizard for a brand-new project. Once a base exists, later named-environment generations only ever touch that one override file.

The wizard will never let you regenerate an existing base config with a minimal one — that would silently delete every other setting in it. Want to change server IPs in an existing base file? Edit it by hand, or add a named environment override instead.

Customizing the commands

ruby provision <env> and bin/rails console come straight from the original Rails-flavored kamal_menu() script this plugin is based on. If your stack differs, edit the case "$ACTION" branches in scripts/dispatch.sh — it's plain bash, one kamal ... (or arbitrary command) per action, shared by both scripts/run.sh (terminal actions) and scripts/run-background.sh (background actions), so a change here applies to whichever of the two a given action actually runs through. (The original script's Rack::Attack status check isn't included — too specific to one app to generalize; add it back the same way if you use it.)

Permissions & dependencies

  • Requires kamal, jq, find, md5sum, awk, and ruby (stdlib erb + json, no gems) on PATH (all standard on an Omarchy/Arch install with Kamal already set up). If kamal isn't found when an action runs, both scripts/run.sh and scripts/run-background.sh fail with a clear error rather than installing it for you — the plugin never runs gem install on your behalf.
  • Every background action fires one notify-send call when it finishes (standard on any Omarchy/Arch install, part of libnotify) — silently skipped if it's missing, the same way every other best-effort call in this plugin degrades.
  • Setup and Deploy additionally require the docker CLI on PATH and a reachable Docker daemon (docker info) — checked before either one spawns kamal, since both build and push an image from this machine.
  • Runs bash scripts/discover.sh <folders...> to scan the filesystem and cache results at $XDG_RUNTIME_DIR/eduard.kamal-deploy/targets.json (or /tmp/eduard.kamal-deploy-$UID if $XDG_RUNTIME_DIR isn't set — that fallback directory's ownership and permissions are checked, refusing to read or write through it if it's not exactly this user's own 0700 directory, since /tmp is shared). The file itself is never opened directly for writing (a fresh temp file is written instead and rename(2)d into place — atomic, and replaces a symlink at that path rather than following it) and every read is checked for a symlink first and capped at 4 MiB.
  • Every path field runs bash scripts/complete-path.sh <partial> (read-only directory listing) on Tab, restricted to $HOME the same way every write path in this plugin is.
  • Runs bash scripts/run.sh <target> <action> [accessory] inside your default terminal for Tail logs / Rails console / Bash shell / Rollback / an accessory's own Logs — real kamal, ruby, and shell commands, with your real credentials and SSH keys. Every other action runs the same way through bash scripts/run-background.sh <target> <action> [accessory] instead, captured (not shown live) by the panel rather than opened in a terminal — same commands, same credentials, just no TTY; it refuses to run any of the terminal-only actions itself as a second guard against that mismatch. Review scripts/dispatch.sh (what each action actually runs) before installing if that matters to you.
  • The Provision Wizard runs bash scripts/provision-check.sh <folder> [env] (read-only) and bash scripts/generate-provision.sh <folder> <options>, which writes <folder>/provision (overwriting any existing file with that name) and marks it executable, and/or bash scripts/generate-deploy.sh <folder> <env> <options>, which writes <folder>/config/deploy.yml or <folder>/config/deploy.<env>.yml (refusing to touch an existing base config — see Deploy config above). Neither ever opens the destination path directly: a base config is written with O_EXCL (atomically fails if anything — file or symlink — is already there, rather than trusting an earlier existence check); provision and a named-environment override, which are meant to be regenerable, are written to a fresh temp file and rename(2)d into place, which replaces a symlink instead of following it.
  • Reads/writes "eduard.kamal-deploy".searchPaths in ~/.config/omarchy/shell.json.
  • Once per panel-open, runs bash scripts/check-update.sh (read-only) — one curl request (capped at 1 MiB) to raw.githubusercontent.com for this repo's own manifest.json, silent on any failure including offline. Clicking the update icon it can reveal only displays instructions — nothing is fetched, run, or written on click.
  • Like every Quickshell plugin, Panel.qml runs unsandboxed inside the shared omarchy-shell process — review it before installing.

Remove

omarchy plugin remove eduard.kamal-deploy

This deletes ~/.config/omarchy/plugins/eduard.kamal-deploy/ and removes the widget from your bar layout. It does not revert the "eduard.kamal-deploy" key in shell.json — delete it by hand, or run omarchy refresh shell, if you want it gone too.

Files

File Purpose
manifest.json Plugin manifest (bar-widget)
Panel.qml Bar icon + task-list panel UI + Provision Wizard
scripts/discover.sh Scans search folders for config/deploy*.yml, prints/caches JSON
scripts/cache-dir.sh Resolves + validates the targets-cache directory, sourced by the three scripts below
scripts/ensure-kamal.sh Installs the kamal gem if it's missing, sourced by run.sh and run-background.sh
scripts/dispatch.sh Shared action→kamal/ruby command mapping, sourced by both scripts below
scripts/run.sh Resolves a clicked target, runs it in a real terminal (interactive/streaming actions)
scripts/run-background.sh Same resolution, runs in the background instead, output captured for the panel
scripts/complete-path.sh Read-only Tab-completion for the panel's folder fields
scripts/check-update.sh Read-only: compares installed vs. GitHub manifest.json version
scripts/detect-common.sh Lists common project-folder names that exist under $HOME
scripts/provision-check.sh Read-only pre-flight checks for the Provision Wizard
scripts/generate-provision.sh Renders templates/provision.erb into <folder>/provision
templates/provision.erb The parameterized provisioning script itself
scripts/generate-deploy.sh Renders templates/deploy.erb (+ deploy-tail.yml for a new base config) into config/deploy.yml or config/deploy.<env>.yml
templates/deploy.erb The service/image/servers: header this wizard controls
templates/deploy-tail.yml Verbatim copy of kamal init's proxy/registry/builder/… guidance

License

MIT — see LICENSE.