Skip to content

Writing Plugins

External modules are standalone executables that communicate with glidesh via newline-delimited JSON on stdin/stdout. They can be written in any language — scripted or compiled. Plugins never receive SSH credentials — they request operations through glidesh, which proxies them over the existing SSH session.

The protocol has three phases:

  1. Describe — glidesh probes the plugin during discovery to learn its name and version. This happens once at startup in a short-lived process. The describe handshake is only used during discovery — runtime check/apply processes do not receive a describe request.
  2. Check/Apply — glidesh spawns a new plugin process and sends a task; the plugin may issue SSH requests and returns a result
  3. Shutdown — glidesh tells the plugin to exit

All messages are single-line JSON, terminated by a newline.

glidesh sends:

{"method":"describe"}

Plugin responds:

{"name":"acme/nginx-vhost","version":"1.0.0","protocol_version":1}
FieldTypeDescription
namestringCanonical module name. May contain / for owner/name format
versionstringModule version (informational)
protocol_versionintegerMust be 1

The name in the describe response is authoritative — it becomes the name used in plans with external "acme/nginx-vhost".

After the handshake, glidesh sends check or apply requests:

{
"method": "check",
"resource_name": "mysite",
"args": {
"server_name": {"string": "example.com"},
"listen": {"integer": 443}
},
"os_info": {
"id": "ubuntu",
"version": "22.04",
"family": "debian",
"pkg_manager": "apt",
"init_system": "systemd",
"container_runtime": "docker" // may be null if no runtime detected
},
"vars": {"host.name": "web-1", "host.address": "10.0.0.1"},
"dry_run": false
}

Return one of three statuses:

{"status":"satisfied"}
{"status":"pending","plan":"will create /etc/nginx/sites-enabled/mysite"}
{"status":"unknown","reason":"cannot determine nginx state"}
{
"changed": true,
"output": "created vhost mysite",
"stderr": "",
"exit_code": 0
}

If something goes wrong, return an error at any point:

{"error":"nginx config syntax check failed"}

Plugins don’t have direct SSH access. Instead, they send SSH operation requests and glidesh responds with results. This proxy loop can repeat as many times as needed during a single check or apply call.

Request:

{"ssh":"exec","command":"nginx -t"}

Response:

{"ssh_result":"exec","exit_code":0,"stdout":"syntax ok\n","stderr":""}

Request:

{"ssh":"upload","path":"/etc/nginx/sites-enabled/mysite","content_base64":"c2VydmVyIHsgLi4uIH0="}

Response:

{"ssh_result":"upload","ok":true}

On failure, an error field is included:

{"ssh_result":"upload","ok":false,"error":"Permission denied"}

Request:

{"ssh":"download","path":"/etc/nginx/sites-enabled/mysite"}

Response:

{"ssh_result":"download","content_base64":"c2VydmVyIHsgLi4uIH0=","exists":true}

When the file doesn’t exist or an error occurs, exists is false and an optional error field explains why:

{"ssh_result":"download","content_base64":"","exists":false,"error":"No such file"}

Request:

{"ssh":"checksum","path":"/etc/nginx/sites-enabled/mysite"}

Response:

{"ssh_result":"checksum","hash":"a1b2c3...","exists":true}

When the file doesn’t exist: {"ssh_result":"checksum","hash":"","exists":false}. On other failures, an error field is included.

set_attrs — Set file ownership and permissions

Section titled “set_attrs — Set file ownership and permissions”

Request:

{"ssh":"set_attrs","path":"/etc/nginx/sites-enabled/mysite","owner":"root","group":"root","mode":"0644"}

Response:

{"ssh_result":"set_attrs","ok":true}

On failure: {"ssh_result":"set_attrs","ok":false,"error":"Permission denied"}.

All fields in set_attrs except path are optional — only provided fields are changed.

glidesh sends a shutdown request when it no longer needs the plugin:

{"method":"shutdown"}

The plugin should exit cleanly.

All four implementations below do the same thing — set /etc/motd on the target host. The plugin:

  1. describe — reports itself as example/motd with protocol version 1
  2. check — downloads /etc/motd via SSH proxy and compares to the desired content
  3. apply — uploads the new content via SSH proxy
  4. shutdown — exits cleanly

Save the file as glidesh-module-example-motd (compiled languages produce a binary with this name), place it in one of the discovery paths, and use it in a plan:

step "Set MOTD" {
external "example/motd" "Managed by glidesh."
}
#!/usr/bin/env bash
set -euo pipefail
MOTD_PATH="/etc/motd"
while IFS= read -r line; do
method=$(echo "$line" | jq -r '.method // empty')
case "$method" in
describe)
echo '{"name":"example/motd","version":"1.0.0","protocol_version":1}'
;;
check)
resource=$(echo "$line" | jq -r '.resource_name')
desired="$resource"$'\n'
echo "{\"ssh\":\"download\",\"path\":\"$MOTD_PATH\"}"
IFS= read -r result
exists=$(echo "$result" | jq -r '.exists')
if [ "$exists" = "false" ]; then
echo "{\"status\":\"pending\",\"plan\":\"will create $MOTD_PATH\"}"
else
current=$(echo "$result" | jq -r '.content_base64' | base64 -d)
if [ "$current" = "$desired" ]; then
echo '{"status":"satisfied"}'
else
echo "{\"status\":\"pending\",\"plan\":\"will update $MOTD_PATH\"}"
fi
fi
;;
apply)
resource=$(echo "$line" | jq -r '.resource_name')
content=$(printf '%s\n' "$resource" | base64 -w0)
echo "{\"ssh\":\"upload\",\"path\":\"$MOTD_PATH\",\"content_base64\":\"$content\"}"
IFS= read -r _result
echo "{\"changed\":true,\"output\":\"set $MOTD_PATH\",\"stderr\":\"\",\"exit_code\":0}"
;;
shutdown)
exit 0
;;
esac
done

Requires jq and base64 on the machine running glidesh.

ConventionDetails
Executable prefixglidesh-module- (required for discovery)
Name formatowner/name recommended for distribution
Protocol versionMust be 1
Describe responseMust return non-empty name
DistributionPlace in ./modules/ (relative to inventory) or ~/.glidesh/modules/

glidesh runs every plugin process inside a sandbox:

MeasurePlatformEffect
Env scrubAllOnly PATH, HOME/USERPROFILE, LANG, temp dir vars are passed. Secrets are stripped.
Temp workdirAllPlugin CWD is the system temp directory, not the project directory.
Session isolationUnixsetsid() — plugin cannot signal glidesh’s process group.
Filesystem restrictionLinux 5.13+landlock limits access to /tmp, /usr, /lib, /lib64. Home dirs, project files, and SSH keys are blocked.

Runtime (check/apply) processes also receive GLIDESH_PROTOCOL_VERSION and GLIDESH_MODULE_NAME environment variables.

  • Use dry_run in the check/apply request to avoid side effects during --dry-run runs
  • The os_info field tells you the target OS, package manager, and init system — use it to adapt behavior
  • Template variables from the plan are available in vars — the plugin receives them already interpolated
  • Keep plugins stateless between check and apply — glidesh may call them in any order
  • Return descriptive plan messages in Pending responses — they appear in dry-run output