aaif-goose/goose · error · anyhow::Error

--allowed-origin must be a non-wildcard Origin value

Error message

--allowed-origin must be a non-wildcard Origin value

What it means

anyhow bail from 'goose serve' origin validation (crates/goose-cli/src/cli.rs). Each --allowed-origin value is trimmed and rejected if it is empty or exactly '*': CORS origins must be concrete values, and wildcard CORS is not offered via this flag (auth via secret key remains mandatory instead).

Source

Thrown at crates/goose-cli/src/cli.rs:1466

        .map(|secret| secret.trim().to_string())
        .filter(|secret| !secret.is_empty());
    let require_token = env_secret.is_some();
    if !require_token && !dangerously_unauthenticated {
        anyhow::bail!(
            "{GOOSE_SERVER_SECRET_KEY_ENV} must be set to start `goose serve`; pass --dangerously-unauthenticated to run without ACP authentication"
        );
    }
    if dangerously_unauthenticated && !require_token {
        warn!(
            "{GOOSE_SERVER_SECRET_KEY_ENV} is not set and --dangerously-unauthenticated was passed; the ACP endpoint will accept unauthenticated connections"
        );
    }
    let additional_allowed_origins = allowed_origins
        .into_iter()
        .map(|origin| {
            let origin = origin.trim();
            if origin.is_empty() || origin == "*" {
                anyhow::bail!("--allowed-origin must be a non-wildcard Origin value");
            }
            HeaderValue::from_str(origin).map_err(|error| {
                anyhow::anyhow!("invalid --allowed-origin value `{origin}`: {error}")
            })
        })
        .collect::<Result<Vec<_>>>()?;
    let secret_key = env_secret.unwrap_or_else(generate_serve_secret_key);
    if let Err(error) = server.start_scheduler().await {
        warn!("Scheduler failed to start; scheduled jobs will not run until a client connects: {error}");
    }
    let router = create_router(
        server,
        secret_key,
        require_token,
        additional_allowed_origins,
    );

    let config = Config::global();

View on GitHub (pinned to 3810898a74)

Solutions

  1. Pass concrete origins: --allowed-origin http://localhost:1420 --allowed-origin https://app.example.com
  2. If a script builds flags from variables, skip the flag when the variable is empty
  3. Do not rely on '*' — connect clients are authenticated with the GOOSE_SERVER__SECRET_KEY token instead
  4. List each origin you actually serve from (dev server port + production domain)

Example fix

# before
goose serve --allowed-origin '*'

# after
goose serve --allowed-origin http://localhost:1420 --allowed-origin https://app.example.com
Defensive patterns

Strategy: validation

Validate before calling

origin = origin.strip()
if not origin or origin == "*":
    raise SystemExit("--allowed-origin must be a concrete origin like https://app.example.com")

Type guard

def is_concrete_origin(v: str) -> bool:
    v = v.strip()
    return bool(v) and v != "*" and not v.endswith('/')

Prevention

When it happens

Trigger: Running 'goose serve --allowed-origin "*"' or passing an empty/whitespace-only value (e.g. an env expansion that produced an empty string): origin.is_empty() || origin == "*" triggers the bail before HeaderValue parsing.

Common situations: Copy-pasting a permissive CORS setup from another tool; shell loops building --allowed-origin flags from a variable that is sometimes empty; misunderstanding that goose gates cross-origin access via the token, not via CORS wildcard.

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/552ac233cc64278e. Report an issue: GitHub.