charmbracelet/crush · error
failed to create server working directory
Error message
failed to create server working directory: %v
What it means
perHostServerDir builds the per-host server state directory under config.GlobalCacheDir() ('server-<safe-host-name>') and creates it with os.MkdirAll(0o700). Any MkdirAll failure is wrapped as 'failed to create server working directory'. Called by spawnAndWaitReady and startDetachedServer, so any server spawn aborts.
Solutions
- Check that config.GlobalCacheDir()'s path (respecting XDG_CACHE_HOME) exists and is writable by the current user.
- If a regular file occupies the 'server-<host>' path, remove or rename it.
- Free disk space / raise quota if the filesystem is full.
- Fix HOME/XDG_CACHE_HOME environment variables before launching crush.
Example fix
// before
// no pre-check; os.MkdirAll error aborts
chDir, err := perHostServerDir(hostURL)
// after
if info, statErr := os.Stat(config.GlobalCacheDir()); statErr == nil && !info.IsDir() {
return nil, fmt.Errorf("cache dir %s is a file, not a directory", config.GlobalCacheDir())
}
chDir, err := perHostServerDir(hostURL) Defensive patterns
Strategy: validation
Validate before calling
cache := config.GlobalCacheDir()
if info, err := os.Stat(cache); err != nil {
return fmt.Errorf("cache dir missing: %w", err)
} else if !info.IsDir() {
return fmt.Errorf("%s is not a directory", cache)
}
probe := filepath.Join(cache, ".write-test")
if err := os.WriteFile(probe, nil, 0o600); err != nil {
return fmt.Errorf("cache dir not writable: %w", err)
}
os.Remove(probe) Prevention
- Verify XDG_CACHE_HOME/HOME resolve to an existing, writable directory
- Ensure the filesystem has free space and inodes
- Never let a regular file occupy names the tool expects to be directories
- Run as a consistent user so 0o700 dirs remain accessible
When it happens
Trigger: os.MkdirAll fails because the cache dir's parents don't exist and can't be created, permission is denied on an existing parent, the target path exists as a regular file, or the filesystem is full/read-only.
Common situations: XDG_CACHE_HOME/HOME pointing at a nonexistent or unwritable location; a file named like the expected directory left by a bug; read-only container rootfs; disk quota exceeded; running with a different $USER than the cache owner.
Related errors
- create directory
- error listing directory
- failed to check if directory is empty
- failed to create directory for provider cache
- create config directory
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/9bcfe268d0429243.
Report an issue: GitHub.
Appendix: source
Thrown at internal/cmd/root.go:629
// quickHealthProbe issues a single readiness request with the caller's
// context and returns nil iff the server is responsive right now.
func quickHealthProbe(ctx context.Context, hostURL *url.URL) error {
httpClient, reqURL, err := readinessHTTPClient(hostURL)
if err != nil {
return err
}
return probeHealth(ctx, httpClient, reqURL, hostURL)
}
// perHostServerDir returns (and creates) the cache directory used for
// per-host server state (logs, start.lock, etc.). The path is derived
// from the parsed host URL rather than the global flag so the same key
// is computed regardless of where the host came from.
func perHostServerDir(hostURL *url.URL) (string, error) {
chDir := filepath.Join(config.GlobalCacheDir(), "server-"+safeHostName(hostURL))
if err := os.MkdirAll(chDir, 0o700); err != nil {
return "", fmt.Errorf("failed to create server working directory: %v", err)
}
return chDir, nil
}
// safeHostName returns a filesystem-safe identifier for hostURL,
// suitable for use as a directory name. It mirrors the input shape of
// the --host flag so client and server compute the same key.
func safeHostName(hostURL *url.URL) string {
return safeNameRegexp.ReplaceAllString(
hostURL.Scheme+"://"+hostURL.Host+hostURL.Path, "_",
)
}
// serverReadyTimeout returns the total budget for the readiness probe.
// Overridable via CRUSH_SERVER_READY_TIMEOUT (parsed as a Go duration).
func serverReadyTimeout() time.Duration {
const def = 10 * time.Second
v := os.Getenv("CRUSH_SERVER_READY_TIMEOUT")View on GitHub (pinned to 7944b8e522)