Thanks to visit codestin.com
Credit goes to github.com

Skip to content

virtuallytd/hooky

Repository files navigation

Hooky

Test Release Latest Release Go Report Card License

A lightweight HTTP webhook server written in Go. Trigger shell scripts from HTTP requests with built-in secret validation, rate limiting, and configurable execution controls. Runs standalone or in Docker.

Features

  • Secret validation — HMAC-SHA256/SHA1/SHA512 signatures or bearer tokens
  • Trigger rules — composable and/or/not conditions on payload fields, headers, query params, or IP ranges
  • Rate limiting — sliding window per hook
  • Concurrency control — limit simultaneous executions per hook
  • Command timeouts — kill long-running commands automatically
  • Fire-and-forget — return a response immediately and run the script in the background
  • Hot reload — edit your config without restarting (-hotreload or SIGHUP)
  • Proxy-aware — correct client IP resolution behind reverse proxies
  • Structured logging — JSON or text output via log/slog
  • Health endpoint/health
  • No secret in config — use env:VAR or file:/path to keep secrets out of config files

Installation

Binary — download the latest release for your platform from the releases page:

tar -xzf hooky_*_linux_amd64.tar.gz
sudo mv hooky /usr/local/bin/

Docker:

docker pull ghcr.io/virtuallytd/hooky:latest

From source:

go install hooky@latest

Quick Start

  1. Create a hooks.yaml:
hooks:
  - id: deploy
    command: /scripts/deploy.sh
    secret:
      type: hmac-sha256
      header: X-Hub-Signature-256
      value: env:DEPLOY_SECRET
  1. Run:
DEPLOY_SECRET=mysecret hooky -hooks hooks.yaml
  1. Trigger it:
BODY='{"ref":"main"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "mysecret" | awk '{print $2}')

curl -X POST http://localhost:9000/hooks/deploy \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=$SIG" \
  -d "$BODY"

Configuration

Hooks are defined in a YAML or JSON file. Pass the path with -hooks.

hooks:
  - id: string                  # URL endpoint: /hooks/{id}
    command: string             # Executable to run
    working-dir: string         # Working directory for the command
    timeout: duration           # e.g. 30s, 5m (default: 30s)
    http-methods: [POST]        # Allowed HTTP methods (default: [POST])
    fire-and-forget: false      # Return 200 immediately, run script in background
    max-concurrent: 0           # Max simultaneous executions (0 = unlimited)

    secret:                     # Validate the incoming request
      type: hmac-sha256         # hmac-sha1 | hmac-sha256 | hmac-sha512 | token
      header: X-Hub-Signature-256
      query: token              # Alternative: read token from query parameter
      value: env:MY_SECRET      # env:VAR, file:/path, or a literal string

    trigger-rule:               # Additional conditions (optional)
      and:
        - match:
            type: value         # value | regex | ip-whitelist | payload-hmac-sha256 | ...
            parameter:
              source: payload   # payload | header | query | request | raw-body
              name: event       # dot-notation for nested fields: repository.full_name
            value: push

    args:                       # Positional arguments passed to the command
      - source: payload
        name: ref

    env:                        # Environment variables for the command
      - name: GIT_REF
        source: payload         # payload | header | query | env | literal
        key: ref

    response:
      success-code: 200
      error-code: 500
      mismatch-code: 403        # Returned when secret or trigger rules fail
      message: "Triggered."
      include-output: false     # Stream stdout/stderr back to the caller
      headers:
        X-Custom: value

    rate-limit:
      requests: 10
      window: 1m

Parameter Sources

Source Description
payload JSON body field. Supports dot-notation: repository.full_name
header HTTP request header
query URL query parameter
request Request metadata. Supported names: remote-addr
raw-body The raw, unparsed request body
literal A hard-coded string value (use name as the value)
entire-payload The full JSON body as a string
entire-headers All headers serialised as JSON
entire-query All query parameters serialised as JSON

Secret Resolution

The value field in secret and trigger rule secret fields supports three formats:

Format Description
env:MY_VAR Read from the $MY_VAR environment variable
file:/run/secrets/token Read from a file (whitespace trimmed)
literal-value Used as-is

Trigger Rules

Rules can be nested with and, or, and not:

trigger-rule:
  and:
    - match:
        type: value
        parameter: {source: payload, name: event}
        value: push
    - or:
        - match:
            type: regex
            parameter: {source: payload, name: ref}
            value: ^refs/heads/main$
        - match:
            type: ip-whitelist
            ip-range: 10.0.0.0/8

Match types:

Type Description
value Exact string match
regex Go regular expression match
ip-whitelist CIDR range check (uses ip-range field)
payload-hmac-sha1 HMAC-SHA1 signature of the raw body
payload-hmac-sha256 HMAC-SHA256 signature of the raw body
payload-hmac-sha512 HMAC-SHA512 signature of the raw body

CLI Options

-hooks string        Path to hooks config file, JSON or YAML (default: hooks.yaml)
-addr string         Address to listen on (default: :9000)
-prefix string       URL prefix for hook endpoints (default: hooks)
-cert string         TLS certificate file — enables HTTPS when set
-key string          TLS private key file
-hotreload           Watch config file and reload on change
-log-format string   Log format: text | json (default: text)
-log-level string    Log level: debug | info | warn | error (default: info)
-proxy-header string Header to use for the real client IP (e.g. X-Forwarded-For)
-version             Print version and exit

Deployment

Standalone

Hooky listens on port 9000 by default. Change it with -addr:

hooky -hooks hooks.yaml -addr :8080

Behind a reverse proxy (recommended) — run hooky on localhost and let nginx or Caddy handle TLS and public traffic. Pass -proxy-header X-Forwarded-For so IP whitelist rules see the real client IP:

hooky -hooks hooks.yaml -proxy-header X-Forwarded-For

Example nginx config:

location / {
    proxy_pass http://localhost:9000;
    proxy_set_header X-Forwarded-For $remote_addr;
}

With built-in TLS — hooky can terminate TLS directly without a reverse proxy:

hooky -hooks hooks.yaml -cert cert.pem -key key.pem

systemd Service

A systemd unit file is provided in init/systemd/hooky.service.

1. Create a dedicated user:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin hooky

2. Install the binary:

sudo mv hooky /usr/local/bin/hooky
sudo chmod +x /usr/local/bin/hooky

3. Create the config directory and add your files:

sudo mkdir -p /etc/hooky /opt/hooky/scripts
sudo cp hooks.yaml /etc/hooky/hooks.yaml
sudo cp .env.example /etc/hooky/.env
# edit /etc/hooky/.env with your real secrets
sudo chown -R hooky:hooky /etc/hooky
sudo chmod 750 /etc/hooky
sudo chmod 640 /etc/hooky/.env
sudo chown -R root:hooky /opt/hooky/scripts
sudo chmod 750 /opt/hooky/scripts

4. Install and start the service:

sudo cp init/systemd/hooky.service /etc/systemd/system/hooky.service
sudo systemctl daemon-reload
sudo systemctl enable --now hooky

5. Check it is running:

sudo systemctl status hooky
sudo journalctl -u hooky -f

Note: If your scripts need to run Docker commands, add the hooky user to the docker group:

sudo usermod -aG docker hooky

File Locations

Path Purpose
/usr/local/bin/hooky Binary
/etc/hooky/hooks.yaml Hook configuration
/etc/hooky/.env Secrets and environment variables
/etc/systemd/system/hooky.service Systemd unit file
/opt/hooky/scripts/ Hook scripts

Logs

Hooky writes structured output to stdout which systemd captures automatically. Logs are managed by journald — no separate log files or log rotation needed.

# Follow live logs
sudo journalctl -u hooky -f

# Show logs since last boot
sudo journalctl -u hooky -b

# Show logs for a specific time range
sudo journalctl -u hooky --since "2026-01-01 00:00:00" --until "2026-01-01 23:59:59"

Each log line includes the hook id so you can filter by a specific hook:

sudo journalctl -u hooky -f | grep "hook=deploy"

The hook id in your hooks.yaml is what appears in logs, so use descriptive names that make log output easy to read — e.g. api-deploy, worker-restart rather than generic names like hook1.

Docker

Pull the hooky image from the GitHub Container Registry (no authentication required — the image is public):

docker pull ghcr.io/virtuallytd/hooky:latest

Run with a config file and scripts directory:

docker run -p 9000:9000 \
  -v ./hooks.yaml:/app/hooks.yaml:ro \
  -v ./scripts:/app/scripts:ro \
  --env-file .env \
  ghcr.io/virtuallytd/hooky:latest

Or use the provided docker-compose.yml:

cp .env.example .env
# edit .env with your real secrets
docker compose up -d

Authenticating to a private registry — if your deploy scripts pull images from a private registry (e.g. a private GitHub Container Registry repository), pass the credentials to hooky via /etc/hooky/.env so the deploy script can log in before pulling:

# /etc/hooky/.env — registry credentials for the deploy script
REGISTRY=ghcr.io
REGISTRY_USER=myorg
REGISTRY_TOKEN=ghp_xxxxxxxxxxxx   # GitHub PAT with read:packages scope

The deploy script can then authenticate using these variables:

echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
docker compose pull myservice

See the examples/ directory for a complete worked example including this auth pattern.

If your scripts need to control other containers on the host, mount the Docker socket:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

Warning: Mounting the Docker socket gives the container full control over the host's Docker daemon. Ensure the server is not publicly accessible without authentication.

Examples

A complete end-to-end example — deploy script, hooky config, and GitHub Actions workflow — is available in the examples/ directory.

Releases

Releases are automated via GitHub Actions and GoReleaser. To cut a release:

git tag v1.0.0
git push origin v1.0.0

This triggers the release workflow which:

  • Builds binaries for linux/amd64 and linux/arm64
  • Creates a GitHub release with archives and a checksums.txt
  • Builds a multi-arch Docker image and pushes it to ghcr.io/virtuallytd/hooky

The Docker image is tagged with both the version (v1.0.0) and latest.

Testing

# Run all tests
go test ./...

# Run a specific package
go test ./internal/hook/...

# Run a single test
go test ./internal/server/... -run TestHook_HMAC_Valid

# Run with verbose output
go test ./... -v

# Run with the race detector
go test -race ./...

What the tests cover

Package Coverage
internal/config YAML and JSON loading, default values, validation (missing ID/command, duplicate IDs), env: and file: secret resolution
internal/hook Parameter extraction from all sources (payload with dot-notation, header, query, raw-body), HMAC-SHA1/256/512 validation, token auth with Bearer prefix stripping, all trigger rule types (value, regex, ip-whitelist, payload-hmac-*), boolean rule composition (and/or/not), rate limiting (allow, block, window reset), command execution (success, failure, exit codes, timeout, working directory, env var passing, concurrency limits, fire-and-forget)
internal/server Full HTTP request lifecycle — routing, method enforcement, secret validation, trigger rules, rate limiting, custom response headers, proxy IP resolution, hot reload via SetConfig, graceful shutdown, end-to-end test from config file on disk through to command output

Tests run in CI on every push and pull request via GitHub Actions.

Contributing

See CONTRIBUTING.md.

License

MIT

About

A lightweight HTTP webhook server written in Go.

Topics

Resources

License

Contributing

Stars

6 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors