{"id":49125603,"url":"https://github.com/virtuallytd/hooky","last_synced_at":"2026-04-21T14:08:46.982Z","repository":{"id":351345777,"uuid":"1186329416","full_name":"virtuallytd/hooky","owner":"virtuallytd","description":"A lightweight HTTP webhook server written in Go.","archived":false,"fork":false,"pushed_at":"2026-04-14T15:36:20.000Z","size":96,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-14T17:24:53.843Z","etag":null,"topics":["go","golang","self-hosted","webhook","webhook-server"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/virtuallytd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-19T14:09:48.000Z","updated_at":"2026-04-14T15:36:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/virtuallytd/hooky","commit_stats":null,"previous_names":["virtuallytd/hooky"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/virtuallytd/hooky","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virtuallytd%2Fhooky","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virtuallytd%2Fhooky/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virtuallytd%2Fhooky/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virtuallytd%2Fhooky/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/virtuallytd","download_url":"https://codeload.github.com/virtuallytd/hooky/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virtuallytd%2Fhooky/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32095213,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T11:25:29.218Z","status":"ssl_error","status_checked_at":"2026-04-21T11:25:28.499Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","self-hosted","webhook","webhook-server"],"created_at":"2026-04-21T14:08:45.097Z","updated_at":"2026-04-21T14:08:46.970Z","avatar_url":"https://github.com/virtuallytd.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hooky\n\n[![Test](https://github.com/virtuallytd/hooky/actions/workflows/test.yml/badge.svg)](https://github.com/virtuallytd/hooky/actions/workflows/test.yml)\n[![Release](https://github.com/virtuallytd/hooky/actions/workflows/release.yml/badge.svg)](https://github.com/virtuallytd/hooky/actions/workflows/release.yml)\n[![Latest Release](https://img.shields.io/github/v/release/virtuallytd/hooky)](https://github.com/virtuallytd/hooky/releases/latest)\n[![Go Report Card](https://goreportcard.com/badge/github.com/virtuallytd/hooky)](https://goreportcard.com/report/github.com/virtuallytd/hooky)\n[![License](https://img.shields.io/github/license/virtuallytd/hooky)](LICENSE)\n\nA 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.\n\n## Features\n\n- **Secret validation** — HMAC-SHA256/SHA1/SHA512 signatures or bearer tokens\n- **Trigger rules** — composable `and`/`or`/`not` conditions on payload fields, headers, query params, or IP ranges\n- **Rate limiting** — sliding window per hook\n- **Concurrency control** — limit simultaneous executions per hook\n- **Command timeouts** — kill long-running commands automatically\n- **Fire-and-forget** — return a response immediately and run the script in the background\n- **Hot reload** — edit your config without restarting (`-hotreload` or `SIGHUP`)\n- **Proxy-aware** — correct client IP resolution behind reverse proxies\n- **Structured logging** — JSON or text output via `log/slog`\n- **Health endpoint** — `/health`\n- **No secret in config** — use `env:VAR` or `file:/path` to keep secrets out of config files\n\n## Installation\n\n**Binary** — download the latest release for your platform from the [releases page](https://github.com/virtuallytd/hooky/releases):\n\n```bash\ntar -xzf hooky_*_linux_amd64.tar.gz\nsudo mv hooky /usr/local/bin/\n```\n\n**Docker:**\n```bash\ndocker pull ghcr.io/virtuallytd/hooky:latest\n```\n\n**From source:**\n```bash\ngo install hooky@latest\n```\n\n## Quick Start\n\n1. Create a `hooks.yaml`:\n\n```yaml\nhooks:\n  - id: deploy\n    command: /scripts/deploy.sh\n    secret:\n      type: hmac-sha256\n      header: X-Hub-Signature-256\n      value: env:DEPLOY_SECRET\n```\n\n2. Run:\n\n```bash\nDEPLOY_SECRET=mysecret hooky -hooks hooks.yaml\n```\n\n3. Trigger it:\n\n```bash\nBODY='{\"ref\":\"main\"}'\nSIG=$(echo -n \"$BODY\" | openssl dgst -sha256 -hmac \"mysecret\" | awk '{print $2}')\n\ncurl -X POST http://localhost:9000/hooks/deploy \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Hub-Signature-256: sha256=$SIG\" \\\n  -d \"$BODY\"\n```\n\n## Configuration\n\nHooks are defined in a YAML or JSON file. Pass the path with `-hooks`.\n\n```yaml\nhooks:\n  - id: string                  # URL endpoint: /hooks/{id}\n    command: string             # Executable to run\n    working-dir: string         # Working directory for the command\n    timeout: duration           # e.g. 30s, 5m (default: 30s)\n    http-methods: [POST]        # Allowed HTTP methods (default: [POST])\n    fire-and-forget: false      # Return 200 immediately, run script in background\n    max-concurrent: 0           # Max simultaneous executions (0 = unlimited)\n\n    secret:                     # Validate the incoming request\n      type: hmac-sha256         # hmac-sha1 | hmac-sha256 | hmac-sha512 | token\n      header: X-Hub-Signature-256\n      query: token              # Alternative: read token from query parameter\n      value: env:MY_SECRET      # env:VAR, file:/path, or a literal string\n\n    trigger-rule:               # Additional conditions (optional)\n      and:\n        - match:\n            type: value         # value | regex | ip-whitelist | payload-hmac-sha256 | ...\n            parameter:\n              source: payload   # payload | header | query | request | raw-body\n              name: event       # dot-notation for nested fields: repository.full_name\n            value: push\n\n    args:                       # Positional arguments passed to the command\n      - source: payload\n        name: ref\n\n    env:                        # Environment variables for the command\n      - name: GIT_REF\n        source: payload         # payload | header | query | env | literal\n        key: ref\n\n    response:\n      success-code: 200\n      error-code: 500\n      mismatch-code: 403        # Returned when secret or trigger rules fail\n      message: \"Triggered.\"\n      include-output: false     # Stream stdout/stderr back to the caller\n      headers:\n        X-Custom: value\n\n    rate-limit:\n      requests: 10\n      window: 1m\n```\n\n### Parameter Sources\n\n| Source | Description |\n|--------|-------------|\n| `payload` | JSON body field. Supports dot-notation: `repository.full_name` |\n| `header` | HTTP request header |\n| `query` | URL query parameter |\n| `request` | Request metadata. Supported names: `remote-addr` |\n| `raw-body` | The raw, unparsed request body |\n| `literal` | A hard-coded string value (use `name` as the value) |\n| `entire-payload` | The full JSON body as a string |\n| `entire-headers` | All headers serialised as JSON |\n| `entire-query` | All query parameters serialised as JSON |\n\n### Secret Resolution\n\nThe `value` field in `secret` and trigger rule `secret` fields supports three formats:\n\n| Format | Description |\n|--------|-------------|\n| `env:MY_VAR` | Read from the `$MY_VAR` environment variable |\n| `file:/run/secrets/token` | Read from a file (whitespace trimmed) |\n| `literal-value` | Used as-is |\n\n### Trigger Rules\n\nRules can be nested with `and`, `or`, and `not`:\n\n```yaml\ntrigger-rule:\n  and:\n    - match:\n        type: value\n        parameter: {source: payload, name: event}\n        value: push\n    - or:\n        - match:\n            type: regex\n            parameter: {source: payload, name: ref}\n            value: ^refs/heads/main$\n        - match:\n            type: ip-whitelist\n            ip-range: 10.0.0.0/8\n```\n\n**Match types:**\n\n| Type | Description |\n|------|-------------|\n| `value` | Exact string match |\n| `regex` | Go regular expression match |\n| `ip-whitelist` | CIDR range check (uses `ip-range` field) |\n| `payload-hmac-sha1` | HMAC-SHA1 signature of the raw body |\n| `payload-hmac-sha256` | HMAC-SHA256 signature of the raw body |\n| `payload-hmac-sha512` | HMAC-SHA512 signature of the raw body |\n\n## CLI Options\n\n```\n-hooks string        Path to hooks config file, JSON or YAML (default: hooks.yaml)\n-addr string         Address to listen on (default: :9000)\n-prefix string       URL prefix for hook endpoints (default: hooks)\n-cert string         TLS certificate file — enables HTTPS when set\n-key string          TLS private key file\n-hotreload           Watch config file and reload on change\n-log-format string   Log format: text | json (default: text)\n-log-level string    Log level: debug | info | warn | error (default: info)\n-proxy-header string Header to use for the real client IP (e.g. X-Forwarded-For)\n-version             Print version and exit\n```\n\n## Deployment\n\n### Standalone\n\nHooky listens on port `9000` by default. Change it with `-addr`:\n\n```bash\nhooky -hooks hooks.yaml -addr :8080\n```\n\n**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:\n\n```bash\nhooky -hooks hooks.yaml -proxy-header X-Forwarded-For\n```\n\nExample nginx config:\n\n```nginx\nlocation / {\n    proxy_pass http://localhost:9000;\n    proxy_set_header X-Forwarded-For $remote_addr;\n}\n```\n\n**With built-in TLS** — hooky can terminate TLS directly without a reverse proxy:\n\n```bash\nhooky -hooks hooks.yaml -cert cert.pem -key key.pem\n```\n\n### systemd Service\n\nA systemd unit file is provided in [`init/systemd/hooky.service`](init/systemd/hooky.service).\n\n**1. Create a dedicated user:**\n\n```bash\nsudo useradd --system --no-create-home --shell /usr/sbin/nologin hooky\n```\n\n**2. Install the binary:**\n\n```bash\nsudo mv hooky /usr/local/bin/hooky\nsudo chmod +x /usr/local/bin/hooky\n```\n\n**3. Create the config directory and add your files:**\n\n```bash\nsudo mkdir -p /etc/hooky /opt/hooky/scripts\nsudo cp hooks.yaml /etc/hooky/hooks.yaml\nsudo cp .env.example /etc/hooky/.env\n# edit /etc/hooky/.env with your real secrets\nsudo chown -R hooky:hooky /etc/hooky\nsudo chmod 750 /etc/hooky\nsudo chmod 640 /etc/hooky/.env\nsudo chown -R root:hooky /opt/hooky/scripts\nsudo chmod 750 /opt/hooky/scripts\n```\n\n**4. Install and start the service:**\n\n```bash\nsudo cp init/systemd/hooky.service /etc/systemd/system/hooky.service\nsudo systemctl daemon-reload\nsudo systemctl enable --now hooky\n```\n\n**5. Check it is running:**\n\n```bash\nsudo systemctl status hooky\nsudo journalctl -u hooky -f\n```\n\n\u003e **Note:** If your scripts need to run Docker commands, add the `hooky` user to the `docker` group:\n\u003e ```bash\n\u003e sudo usermod -aG docker hooky\n\u003e ```\n\n#### File Locations\n\n| Path | Purpose |\n|------|---------|\n| `/usr/local/bin/hooky` | Binary |\n| `/etc/hooky/hooks.yaml` | Hook configuration |\n| `/etc/hooky/.env` | Secrets and environment variables |\n| `/etc/systemd/system/hooky.service` | Systemd unit file |\n| `/opt/hooky/scripts/` | Hook scripts |\n\n#### Logs\n\nHooky writes structured output to stdout which systemd captures automatically. Logs are managed by `journald` — no separate log files or log rotation needed.\n\n```bash\n# Follow live logs\nsudo journalctl -u hooky -f\n\n# Show logs since last boot\nsudo journalctl -u hooky -b\n\n# Show logs for a specific time range\nsudo journalctl -u hooky --since \"2026-01-01 00:00:00\" --until \"2026-01-01 23:59:59\"\n```\n\nEach log line includes the hook `id` so you can filter by a specific hook:\n\n```bash\nsudo journalctl -u hooky -f | grep \"hook=deploy\"\n```\n\nThe 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`.\n\n### Docker\n\nPull the hooky image from the GitHub Container Registry (no authentication required — the image is public):\n\n```bash\ndocker pull ghcr.io/virtuallytd/hooky:latest\n```\n\nRun with a config file and scripts directory:\n\n```bash\ndocker run -p 9000:9000 \\\n  -v ./hooks.yaml:/app/hooks.yaml:ro \\\n  -v ./scripts:/app/scripts:ro \\\n  --env-file .env \\\n  ghcr.io/virtuallytd/hooky:latest\n```\n\nOr use the provided `docker-compose.yml`:\n\n```bash\ncp .env.example .env\n# edit .env with your real secrets\ndocker compose up -d\n```\n\n**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:\n\n```bash\n# /etc/hooky/.env — registry credentials for the deploy script\nREGISTRY=ghcr.io\nREGISTRY_USER=myorg\nREGISTRY_TOKEN=ghp_xxxxxxxxxxxx   # GitHub PAT with read:packages scope\n```\n\nThe deploy script can then authenticate using these variables:\n\n```bash\necho \"$REGISTRY_TOKEN\" | docker login \"$REGISTRY\" -u \"$REGISTRY_USER\" --password-stdin\ndocker compose pull myservice\n```\n\nSee the [`examples/`](examples/) directory for a complete worked example including this auth pattern.\n\nIf your scripts need to control other containers on the host, mount the Docker socket:\n\n```yaml\nvolumes:\n  - /var/run/docker.sock:/var/run/docker.sock\n```\n\n\u003e **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.\n\n## Examples\n\nA complete end-to-end example — deploy script, hooky config, and GitHub Actions workflow — is available in the [`examples/`](examples/) directory.\n\n## Releases\n\nReleases are automated via GitHub Actions and [GoReleaser](https://goreleaser.com). To cut a release:\n\n```bash\ngit tag v1.0.0\ngit push origin v1.0.0\n```\n\nThis triggers the release workflow which:\n- Builds binaries for `linux/amd64` and `linux/arm64`\n- Creates a GitHub release with archives and a `checksums.txt`\n- Builds a multi-arch Docker image and pushes it to `ghcr.io/virtuallytd/hooky`\n\nThe Docker image is tagged with both the version (`v1.0.0`) and `latest`.\n\n## Testing\n\n```bash\n# Run all tests\ngo test ./...\n\n# Run a specific package\ngo test ./internal/hook/...\n\n# Run a single test\ngo test ./internal/server/... -run TestHook_HMAC_Valid\n\n# Run with verbose output\ngo test ./... -v\n\n# Run with the race detector\ngo test -race ./...\n```\n\n### What the tests cover\n\n| Package | Coverage |\n|---------|----------|\n| `internal/config` | YAML and JSON loading, default values, validation (missing ID/command, duplicate IDs), `env:` and `file:` secret resolution |\n| `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) |\n| `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 |\n\nTests run in CI on every push and pull request via GitHub Actions.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvirtuallytd%2Fhooky","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvirtuallytd%2Fhooky","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvirtuallytd%2Fhooky/lists"}