gastownhall/beads · error

workspacegate: open gate %s: %w

Error message

workspacegate: open gate %s: %w

What it means

Acquire in internal/workspacegate could not open (or create) the gate lock file at g.path with O_CREATE|O_RDWR and 0o600 permissions. The underlying os.PathError is wrapped with %w so the caller can inspect it via errors.Is/As. This is a filesystem-level failure before any advisory locking is attempted, so no lock state was changed.

Source

Thrown at internal/workspacegate/gate.go:392

	// Tolerate a nil context rather than panicking on ctx.Err below: gate
	// acquisition sits on CLI plumbing paths (cobra hooks, migrate helpers)
	// that tests and embedders invoke directly without the process-level
	// signal context, and a nil-deref here kills the whole test binary.
	if ctx == nil {
		ctx = context.Background()
	}
	if err := ctx.Err(); err != nil {
		return nil, fmt.Errorf("workspacegate: acquiring %s: %w", g.path, err)
	}
	poll := opts.PollInterval
	if poll <= 0 {
		poll = 100 * time.Millisecond
	}
	deadline := time.Now().Add(opts.Wait)

	f, err := os.OpenFile(g.path, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("workspacegate: open gate %s: %w", g.path, err)
	}

	try := lockfile.FlockSharedNonBlock
	if mode == Exclusive {
		try = lockfile.FlockExclusiveNonBlock
	}

	notified := false
	for {
		err := try(f)
		if err == nil {
			h := &Handle{gate: g, mode: mode, f: f}
			if mode == Exclusive {
				g.writeInfo(opts.Reason)
			}
			return h, nil
		}
		if !errors.Is(err, lockfile.ErrLockBusy) && !lockfile.IsLocked(err) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the gate path: ensure its parent directory exists (os.MkdirAll) and the path itself is not a directory.
  2. Verify the current user can create/write the file: run ls -ld on the path and parent, or touch the file manually.
  3. If the gate file exists but is owned by another user, remove it or chown/chmod it so the process can open it O_RDWR.
  4. If running in a container/sandbox, mount or configure a writable directory for the gate path.

Example fix

// before
g := workspacegate.New("/nonexistent/dir/gate.lock")
h, err := g.Acquire(ctx, workspacegate.Exclusive, opts)

// after
os.MkdirAll("/var/run/myapp", 0o755)
g := workspacegate.New("/var/run/myapp/gate.lock")
h, err := g.Acquire(ctx, workspacegate.Exclusive, opts)
Defensive patterns

Strategy: try-catch

Validate before calling

if fi, err := os.Stat(dirOfGatePath); err != nil || !fi.IsDir() {
    return fmt.Errorf("gate directory unavailable: %s", dirOfGatePath)
}
if fi, err := os.Stat(gatePath); err == nil && fi.IsDir() {
    return fmt.Errorf("gate path is a directory: %s", gatePath)
}

Try / catch

h, err := g.Acquire(ctx, mode, opts)
var pe *os.PathError
if err != nil {
    if errors.As(err, &pe) && errors.Is(pe.Err, os.ErrPermission) {
        return fmt.Errorf("cannot access gate %s as current user", pe.Path)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Acquire (via mustAcquire) where os.OpenFile on the gate path fails: the parent directory does not exist, the path is a directory, or the process lacks write permission on an existing gate file.

Common situations: Gate path pointing into a nonexistent or read-only directory; running under a different uid than the gate file owner (0o600 means only the owner can open it read-write); gate path accidentally configured as a directory; sandboxed CI containers with restricted mounts.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/131c6fb3308023ad. Report an issue: GitHub.