charmbracelet/crush · error

failed to resolve workspace path: %w

Error message

failed to resolve workspace path: %w

What it means

CreateWorkspace resolves the caller-supplied workspace path via resolveWorkspaceKey before registering it in the backend's dedupe map. Any failure resolving (e.g. empty path, evaluation/symlink/stat failure) is wrapped with this message and aborts workspace creation.

Source

Thrown at internal/backend/backend.go:356

// parameters, or returns an existing workspace if one already exists at
// the same resolved path (first-wins semantics).
//
// args.ClientID must be a valid UUID identifying the calling client;
// the resulting workspace registers a creation hold on behalf of that
// client which is released either by the first SSE attach (which
// converts it into a stream claim) or by the grace window expiring.
func (b *Backend) CreateWorkspace(args proto.Workspace) (*Workspace, proto.Workspace, error) {
	if args.Path == "" {
		return nil, proto.Workspace{}, ErrPathRequired
	}
	clientID, err := validateClientID(args.ClientID)
	if err != nil {
		return nil, proto.Workspace{}, err
	}

	key, err := resolveWorkspaceKey(args.Path)
	if err != nil {
		return nil, proto.Workspace{}, fmt.Errorf("failed to resolve workspace path: %w", err)
	}

	b.mu.Lock()
	if err := b.admitLocked(clientID); err != nil {
		b.mu.Unlock()
		return nil, proto.Workspace{}, err
	}
	// A client is arriving: cancel any pending idle shutdown so we never
	// hand back a workspace on a server that is about to tear itself down.
	b.cancelShutdownLocked()
	if existingID, ok := b.pathIndex[key]; ok {
		if ws, found := b.workspaces.Get(existingID); found {
			// Hold b.mu while registering: teardown also
			// acquires b.mu before tearing the workspace
			// down, so this guarantees the workspace we
			// return cannot be torn out from under us
			// between lookup and registerClient. Lock order
			// here is b.mu -> ws.clientsMu.

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Pass an existing, absolute directory path as args.Path
  2. Create the directory before calling CreateWorkspace
  3. Fix broken symlinks or mount the path into the environment

Example fix

// before
b.CreateWorkspace(ctx, proto.CreateWorkspaceArgs{Path: ""})
// after
abs, _ := filepath.Abs("/home/user/project")
os.MkdirAll(abs, 0o755)
b.CreateWorkspace(ctx, proto.CreateWorkspaceArgs{Path: abs})
Defensive patterns

Strategy: validation

Validate before calling

func ensureWorkspacePath(p string) error {
    if p == "" {
        return errors.New("workspace path is empty")
    }
    abs, err := filepath.Abs(p)
    if err != nil {
        return err
    }
    fi, err := os.Stat(abs)
    if err != nil {
        return fmt.Errorf("workspace path %q: %w", abs, err)
    }
    if !fi.IsDir() {
        return fmt.Errorf("%q is not a directory", abs)
    }
    return nil
}

Try / catch

ws, err := b.CreateWorkspace(ctx, args)
if err != nil && strings.Contains(err.Error(), "failed to resolve workspace path") {
    return fmt.Errorf("cannot open workspace: %w", err)
}

Prevention

When it happens

Trigger: Calling Backend.CreateWorkspace with args.Path that is empty, does not exist, cannot be made absolute, or fails symlink/stat resolution.

Common situations: Passing a relative path from a different working directory; pointing at a deleted directory; symlink to a missing target; running the client from a container with a non-mounted path.

Related errors


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