JuliusBrussee/caveman · error

invalid Claude organization id

Error message

invalid Claude organization id

What it means

The org id is interpolated directly into the request path https://claude.ai/api/organizations/<orgID>/usage. Because it builds a URL from untrusted env input, values containing '/' or '..' are rejected — they could escape the intended path segment (path traversal / hitting a different endpoint). This is a validation guard, not a formatting nicety.

Source

Thrown at proxy/internal/store/usage_import.go:134

	if err := json.Unmarshal([]byte(raw), &v); err != nil {
		return ImportSummary{}, fmt.Errorf("parse Claude usage JSON: %w", err)
	}
	quotas := quotaEventsFromAny("anthropic", "claude_usage_link", "linked_api", time.Now().UTC().Format(time.RFC3339), v)
	n, err := s.InsertQuotaEvents(quotas)
	if err != nil {
		return ImportSummary{}, err
	}
	return ImportSummary{Source: "claude", QuotaImported: n, Basis: "linked_api"}, nil
}

func fetchClaudeUsageJSON() ([]byte, error) {
	sessionKey := os.Getenv("CAVEMAN_CLAUDE_SESSION_KEY")
	orgID := os.Getenv("CAVEMAN_CLAUDE_ORG_ID")
	if sessionKey == "" || orgID == "" {
		return nil, fmt.Errorf("Claude usage refresh needs CAVEMAN_CLAUDE_USAGE_JSON or CAVEMAN_CLAUDE_SESSION_KEY plus CAVEMAN_CLAUDE_ORG_ID")
	}
	if strings.Contains(orgID, "/") || strings.Contains(orgID, "..") {
		return nil, fmt.Errorf("invalid Claude organization id")
	}
	req, err := http.NewRequest("GET", "https://claude.ai/api/organizations/"+orgID+"/usage", nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("accept", "application/json")
	req.Header.Set("cookie", "sessionKey="+sessionKey)
	client := http.Client{Timeout: 15 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("Claude usage request failed with HTTP %d", resp.StatusCode)
	}
	return io.ReadAll(io.LimitReader(resp.Body, 4<<20))
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set CAVEMAN_CLAUDE_ORG_ID to the bare organization uuid with no scheme, slashes, or dots: export CAVEMAN_CLAUDE_ORG_ID=0f1e2d3c-....
  2. Strip accidental slashes before export: CAVEMAN_CLAUDE_ORG_ID=${CAVEMAN_CLAUDE_ORG_ID#/}; verify with echo.
  3. If you only have the full URL, extract the uuid segment (e.g. with parameter expansion or sed) and export just that.

Example fix

# before
export CAVEMAN_CLAUDE_ORG_ID="https://claude.ai/api/organizations/0f1e.../usage"

# after
export CAVEMAN_CLAUDE_ORG_ID="0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0"
Defensive patterns

Strategy: validation

Validate before calling

orgID := os.Getenv("CAVEMAN_CLAUDE_ORG_ID")
if strings.Contains(orgID, "/") || strings.Contains(orgID, "..") || orgID == "" {
    return fmt.Errorf("CAVEMAN_CLAUDE_ORG_ID must be a bare org uuid, got %q", orgID)
}

Type guard

func isBareOrgID(v string) bool {
    return v != "" && !strings.Contains(v, "/") && !strings.Contains(v, "..")
}

Try / catch

if _, err := s.RefreshClaudeUsageFromEnv(); err != nil {
    if strings.Contains(err.Error(), "invalid Claude organization id") {
        return configError("CAVEMAN_CLAUDE_ORG_ID must be the bare uuid — no URL, no slashes")
    }
    return err
}

Prevention

When it happens

Trigger: Setting CAVEMAN_CLAUDE_ORG_ID to a URL, a path fragment ('orgs/123'), a value with a trailing slash, or a copied string that includes '../' — for example pasting the whole organizations URL instead of just the uuid.

Common situations: Pasting 'https://claude.ai/settings/orgs/<uuid>' or '<uuid>/' from the browser address bar instead of the bare uuid; env files that append stray characters; attempting to target a nested API path via the org variable.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/9ddcfc3e9ef73175. Report an issue: GitHub.