Skip to content

Creating Custom Hook Scripts

Hook scripts extend plexd's remote action capabilities without modifying the binary. This guide walks through creating, deploying, and triggering a custom hook on a plexd-managed node.

For the full reference of types and internals, see Remote Actions and Hooks Reference.

Prerequisites

  1. plexd is running on the node with actions enabled (default).

  2. Hooks directory is configured. The default is /etc/plexd/hooks. Set hooks_dir in the actions configuration to override.

  3. Shell access to the node for deploying the script (or a deployment pipeline that places files in the hooks directory).

Step 1: Create the Hook Script

Create a shell script that performs the desired operation. The script receives parameters as PLEXD_PARAM_ prefixed environment variables.

bash
cat > /tmp/restart-service.sh << 'EOF'
#!/bin/sh
set -e

SERVICE="${PLEXD_PARAM_SERVICE}"
if [ -z "$SERVICE" ]; then
    echo "error: SERVICE parameter is required" >&2
    exit 1
fi

echo "Restarting service: $SERVICE"
systemctl restart "$SERVICE"
echo "Service $SERVICE restarted successfully"
EOF

Available Environment Variables

Every hook script has access to the following environment variables:

VariableDescription
PATHInherited from the plexd process
HOMEInherited from the plexd process
PLEXD_NODE_IDID of the node executing the hook
PLEXD_EXECUTION_IDUnique execution ID for this invocation
PLEXD_PARAM_<NAME>Each parameter from the action request

Parameter names are uppercased and non-alphanumeric characters (except underscore) are replaced with underscores. For example, a parameter named service-name becomes PLEXD_PARAM_SERVICE_NAME.

Script Requirements

  • Must have a shebang line (#!/bin/sh, #!/bin/bash, #!/usr/bin/env python3, etc.)
  • Must be executable (chmod +x)
  • Exit code 0 indicates success; non-zero indicates failure
  • Stdout and stderr are captured and sent to the control plane
  • Output is truncated at MaxOutputBytes (default 1 MiB)
  • The script is killed if it exceeds the action timeout

Step 2: Create an Optional Metadata Sidecar

A JSON sidecar file provides metadata about the hook to the control plane. The sidecar file must have the same name as the hook script with a .json extension.

bash
cat > /tmp/restart-service.sh.json << 'EOF'
{
  "description": "Restart a systemd service on the node",
  "parameters": [
    {
      "name": "service",
      "type": "string",
      "required": true,
      "description": "Name of the systemd service to restart"
    }
  ],
  "timeout": "30s",
  "sandbox": "none"
}
EOF

Sidecar Fields

FieldTypeDescription
descriptionstringHuman-readable description of the hook
parameters[]ActionParamList of expected parameters with types
timeoutstringSuggested default timeout (e.g. "30s")
sandboxstringSandbox mode hint (reserved for future use)

Each parameter entry:

FieldTypeDescription
namestringParameter name
typestringType hint (string, bool, int)
requiredboolWhether the parameter is required
descriptionstringHuman-readable description

The sidecar file is optional. If missing or malformed, the hook is still discovered but reported without metadata.

Step 3: Deploy to the Hooks Directory

Copy the script and optional sidecar to the configured hooks directory and ensure the script is executable.

bash
# Copy files
sudo cp /tmp/restart-service.sh /etc/plexd/hooks/restart-service
sudo cp /tmp/restart-service.sh.json /etc/plexd/hooks/restart-service.json

# Set permissions
sudo chmod 755 /etc/plexd/hooks/restart-service
sudo chmod 644 /etc/plexd/hooks/restart-service.json

# Verify
ls -la /etc/plexd/hooks/

Note: The hook name used in action requests is the filename (without extension). In this example, the action name is restart-service.

Step 4: Hook Discovery

plexd automatically discovers new and changed hooks using the HookWatcher, which monitors the hooks directory with fsnotify. No restart is required.

When a hook file is added, modified, or removed, plexd:

  1. Detects the filesystem event (with debouncing)
  2. Scans the file for executability
  3. Computes the SHA-256 checksum
  4. Parses the sidecar metadata file (if present)
  5. Reports updated capabilities to the control plane

The initial scan at startup also follows this process for all existing hooks.

Step 5: Verify Discovery

Check the agent logs to confirm the hook was discovered:

bash
journalctl -u plexd --since "1 minute ago" | grep -i hook

You should see discovery-related log entries. The hook will appear in the capabilities reported to the control plane with its computed checksum.

Verify the Checksum

The control plane receives the hook's SHA-256 checksum. You can verify it locally:

bash
sha256sum /etc/plexd/hooks/restart-service

This checksum must match the value the control plane sends in the action_request event's checksum field. If the checksums don't match at execution time, the hook will fail integrity verification and will not run.

Step 6: Trigger from the Control Plane

The control plane triggers hook execution by sending an action_request SSE event to the node. The event payload contains:

json
{
  "execution_id": "exec-abc-123",
  "action": "restart-service",
  "parameters": {
    "service": "nginx"
  },
  "timeout": "30s",
  "checksum": "a1b2c3d4e5f6..."
}

The node will:

  1. Ack: send an ExecutionAck with status=accepted
  2. Verify: compare the hook's SHA-256 against the provided checksum
  3. Execute: run the script with PLEXD_PARAM_SERVICE=nginx
  4. Report: send an ExecutionResult with stdout, stderr, exit code, and duration

Execution Lifecycle

Control Plane                          Node (plexd)
     │                                      │
     │─── action_request (SSE) ────────────▶│
     │                                      │── parse ActionRequest
     │◀── ExecutionAck (accepted) ──────────│── verify checksum (SHA-256)
     │                                      │── execute script
     │                                      │── capture stdout/stderr
     │◀── ExecutionResult ─────────────────│── report result
     │                                      │

Troubleshooting

Hook Not Discovered

SymptomCauseFix
Hook missing from capabilitiesFile not executablechmod +x /etc/plexd/hooks/my-hook
Hook missing from capabilitieshooks_dir not configuredSet hooks_dir in actions config
Hook missing from capabilitiesFile has .json extensionRemove .json extension from the script filename
Hook missing from capabilitiesFile is in a subdirectoryMove to the hooks directory root (subdirs skipped)

Hook Execution Fails

SymptomCauseFix
Status error, integrity failChecksum mismatchRe-deploy hook and wait for capability refresh
Status error, file not foundHook in capabilities but missingVerify file exists at hooks_dir/<action-name>
Status timeoutScript exceeds timeoutOptimize script or increase timeout in request
Status failed, exit code > 0Script returned non-zero exitCheck stderr in result for error details
Empty stdoutScript writes to file, not stdoutWrite output to stdout (echo) for capture

Parameter Issues

SymptomCauseFix
Empty parameter valueParameter name case mismatchParameters are uppercased: targetPLEXD_PARAM_TARGET
Missing parameterParameter not in action requestEnsure control plane sends the parameter
Garbled parameter nameSpecial characters in nameNon-alphanumeric chars become underscores

Reference

For the full API type definitions, configuration fields, and implementation details, see the Remote Actions and Hooks Reference.