charmbracelet/crush · error

failed to create server working directory: %v

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.

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)

Solutions

  1. Check that config.GlobalCacheDir()'s path (respecting XDG_CACHE_HOME) exists and is writable by the current user.
  2. If a regular file occupies the 'server-<host>' path, remove or rename it.
  3. Free disk space / raise quota if the filesystem is full.
  4. 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

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


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/9bcfe268d0429243. Report an issue: GitHub.