JuliusBrussee/caveman · error

CAVEMAN_AUTH_TOKEN must contain no spaces or control…

Error message

CAVEMAN_AUTH_TOKEN must contain no spaces or control characters

What it means

Config validation error in the proxy's Load path: CAVEMAN_AUTH_TOKEN contains spaces or control characters. Control bytes would terminate the HTTP header field and a space would split scheme from value in 'Authorization: Bearer <token>'; the message deliberately never echoes the secret.

Solutions

  1. Remove spaces and control characters from the token value (trim trailing newline/whitespace).
  2. Regenerate with `openssl rand -hex 32`, which only produces hex digits.
  3. Check the .env file for CRLF line endings (dos2unix) and remove surrounding quotes/whitespace.

Example fix

// before
export CAVEMAN_AUTH_TOKEN="$(cat token.txt)"        # file ends with newline
// after
export CAVEMAN_AUTH_TOKEN="$(tr -d '[:space:]' < token.txt)"
Defensive patterns

Strategy: validation

Validate before calling

token := strings.TrimSpace(os.Getenv("CAVEMAN_AUTH_TOKEN"))
for _, r := range token {
    if r == ' ' || r < 0x20 || r == 0x7f {
        return fmt.Errorf("CAVEMAN_AUTH_TOKEN contains space or control character at %q", token)
    }
}

Prevention

When it happens

Trigger: Setting CAVEMAN_AUTH_TOKEN to a value with internal spaces ("my secret token"), trailing whitespace/newline (e.g. from `echo` without -n, or a CRLF line ending in .env), or an embedded tab.

Common situations: Pasting a secret with a trailing newline; generating tokens with a tool that appends a newline; quoting mistakes in shell assignment leaving literal spaces; Windows-edited config files with CRLF.

Understand the failure class

Background: "is not a valid" / "Invalid ... value" environment variable errors: how libraries validate env vars and what to do when they reject yours — this error's family across 48 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/95644171ab89b8e0. Report an issue: GitHub.

Appendix: source

Thrown at proxy/internal/config/config.go:302

// minAuthTokenBytes is the floor for the inbound shared secret. The token is the
// only gate in front of every configured provider credential once the proxy is
// reachable off-host, so a short one is not a weaker deployment, it is an open one.
const minAuthTokenBytes = 16

// validateAuthToken refuses a token that cannot survive one HTTP header value:
// control bytes terminate the field, and a space would split scheme from value in
// `Authorization: Bearer <token>`. The error never echoes the value — it is a
// secret and this message reaches the proxy log.
func validateAuthToken(token string) error {
	if token == "" {
		return nil
	}
	if len(token) < minAuthTokenBytes {
		return fmt.Errorf("CAVEMAN_AUTH_TOKEN must be at least %d bytes", minAuthTokenBytes)
	}
	for _, r := range token {
		if r == ' ' || r < 0x20 || r == 0x7f {
			return fmt.Errorf("CAVEMAN_AUTH_TOKEN must contain no spaces or control characters")
		}
	}
	return nil
}

// validateListen keeps standalone's BYOK proxy local to one operator unless an
// inbound credential gates it. Binding an empty, wildcard, or non-loopback host
// would expose every configured provider credential to the network with no
// inbound authentication; authenticated says CAVEMAN_AUTH_TOKEN is set, so
// standalone.Auth rejects every request that does not present it and the wider
// bind becomes a deliberate operator choice instead of an accident.
func validateListen(listen string, authenticated bool) error {
	host, port, err := net.SplitHostPort(strings.TrimSpace(listen))
	if err != nil || port == "" {
		return fmt.Errorf("listen address %q must be loopback host:port", listen)
	}
	if strings.EqualFold(host, "localhost") {
		return nil

View on GitHub (pinned to 3ee70a1026)