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. They do not receive the secrets vault either: a plugin sees only the secrets the plan passes it as arguments.

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.
Vault isolationAllSecret-carrying values are stripped from the request’s vars; a plugin sees only what the plan passes it explicitly.
stderr redactionAllPlugin stderr is scrubbed of decrypted secrets before it reaches the debug log.
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, minus any value carrying a secret (pass those explicitly as arguments)
  • 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