zeroclaw-labs/zeroclaw · error · anyhow::Error

webhook channel requires a `secret` configured for request a

Error message

webhook channel requires a `secret` configured for request authentication; set [channels.webhook.{}].secret in config or remove the channel to silence this error

What it means

WebhookChannel::listen refuses to start when [channels.webhook.<alias>] has no secret configured. This is a deliberate fail-closed guard: the inbound HTTP server accepts arbitrary POSTs, and without a secret it would have to accept unauthenticated requests from anyone. The channel errors immediately at startup, naming the alias and the exact TOML key to set, instead of silently running an open endpoint.

Source

Thrown at crates/zeroclaw-channels/src/webhook.rs:349

                            total_attempts,
                            err_msg,
                            delay.as_millis()
                        )
                    );
                    tokio::time::sleep(delay).await;
                }
            }
        }

        unreachable!("send loop exits via return or bail on the final attempt")
    }

    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> Result<()> {
        // Fail-fast: a webhook with no secret accepts *all* incoming requests,
        // including unauthenticated ones.  Refuse to start so the operator is
        // forced to configure a secret.
        if self.secret.is_none() {
            anyhow::bail!(
                "webhook channel requires a `secret` configured for request \
                 authentication; set [channels.webhook.{}].secret in config \
                 or remove the channel to silence this error",
                self.alias,
            );
        }

        use axum::{
            Router,
            body::Bytes,
            extract::State,
            http::{HeaderMap, StatusCode},
            routing::post,
        };
        use portable_atomic::{AtomicU64, Ordering};
        use std::sync::Arc;

        let counter = Arc::new(AtomicU64::new(0));

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set [channels.webhook.<alias>].secret to a long random value: openssl rand -hex 32.
  2. Configure senders to that webhook to sign payloads with the same secret (x-webhook-signature: sha256=<hex HMAC-SHA256 of the raw body>).
  3. If the webhook channel is not actually needed, remove the whole [channels.webhook.<alias>] section so startup proceeds.
  4. If the secret lives in an env-specific overlay, verify the merged config actually contains the key for that alias (zeroclaw config dump).

Example fix

# before
[channels.webhook.main]
send_url = "https://example.com/hook"
listen_path = "/hook"
# listen() -> "webhook channel requires a `secret` configured ... set [channels.webhook.main].secret"

# after
[channels.webhook.main]
send_url = "https://example.com/hook"
listen_path = "/hook"
secret = "9f2c..."   # openssl rand -hex 32; senders must HMAC-SHA256-sign bodies with it
Defensive patterns

Strategy: validation

Validate before calling

// Before starting channels, assert the webhook alias has a non-empty secret.
fn validate_webhook_config(alias: &str, cfg: &WebhookChannelConfig) -> anyhow::Result<()> {
    anyhow::ensure!(
        cfg.secret.as_deref().map(|s| !s.trim().is_empty()).unwrap_or(false),
        "[channels.webhook.{alias}] needs a non-empty `secret` (openssl rand -hex 32)"
    );
    Ok(())
}

Try / catch

match webhook_channel.listen(tx).await {
    Ok(()) => {}
    Err(e) if e.to_string().contains("requires a `secret`") => {
        // Pure config defect: stop, generate a secret, update config, restart.
        // Never catch-and-continue — that would run an unauthenticated endpoint.
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Config declares [channels.webhook.<alias>] with send_url/listen_path but omits secret; secret key set to an empty string; a config generated from an example template that predates the requirement. listen() (channel startup) fails before the axum router binds, so the whole daemon reports the channel as failed.

Common situations: First-time setup following an older example config; splitting config into a template and per-env overlay where the overlay held the secret but was not merged; migrating a dev setup (which used to work secretless) to a newer release that made the secret mandatory.

Understand the failure class

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/6832586accab8954. Report an issue: GitHub.