gastownhall/beads · error

workspacegate: probe %s: %w

Error message

workspacegate: probe %s: %w

What it means

ExclusiveHolder probed the gate by opening the file read-only; the open failed with an error other than NotExist (which is treated as 'never gated'). The os.PathError is wrapped so callers can inspect it. This happens before any flock test, so no holder information is returned.

Source

Thrown at internal/workspacegate/gate.go:584

// spurious failures into concurrent fail-fast acquirers.
//
// The error return is non-nil when the state could not be determined
// (unreadable gate file, unsupported filesystem); callers must not treat
// that as "not held".
func (g Gate) ExclusiveHolder() (held bool, info *Info, err error) {
	if g.path == "" {
		return false, nil, errors.New("workspacegate: zero Gate")
	}
	// O_RDONLY: this is a read-only probe (flock does not require a
	// writable descriptor), and it widens reach — a gate file owned by
	// another user with no write permission for us is still probeable.
	f, err := os.OpenFile(g.path, os.O_RDONLY, 0o600)
	if err != nil {
		if os.IsNotExist(err) {
			// No gate file: nothing has ever gated here.
			return false, nil, nil
		}
		return false, nil, fmt.Errorf("workspacegate: probe %s: %w", g.path, err)
	}
	defer f.Close()

	if lerr := lockfile.FlockSharedNonBlock(f); lerr == nil {
		_ = lockfile.FlockUnlock(f)
		return false, nil, nil
	} else if !errors.Is(lerr, lockfile.ErrLockBusy) && !lockfile.IsLocked(lerr) {
		return false, nil, fmt.Errorf("workspacegate: probe %s: %w", g.path, lerr)
	}
	return true, g.readInfo(), nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run the probe as the same user that created the gate, or chmod the gate file to be group/world readable if cross-user probing is intended.
  2. Verify the path is a regular file (os.Stat, mode check) and not a directory or broken symlink.
  3. Handle os.IsNotExist as the normal 'nothing gated' case and only treat other errors as failures.
  4. Fix path configuration if the gate path was recently moved or renamed.

Example fix

// before
held, _, err := g.ExclusiveHolder(ctx)

// after
if fi, serr := os.Stat(g.Path()); serr == nil && fi.IsDir() {
    return fmt.Errorf("gate path is a directory: %s", g.Path())
}
held, _, err := g.ExclusiveHolder(ctx)
if err != nil {
    var pe *os.PathError
    if errors.As(err, &pe) && errors.Is(pe.Err, os.ErrPermission) {
        return fmt.Errorf("gate unreadable by current user: %w", err)
    }
}
Defensive patterns

Strategy: validation

Validate before calling

fi, err := os.Stat(gatePath)
if os.IsNotExist(err) {
    // nothing has ever gated here — safe to proceed
} else if err != nil {
    return err
} else if !fi.Mode().IsRegular() {
    return fmt.Errorf("gate path is not a regular file: %s", gatePath)
}

Try / catch

held, info, err := g.ExclusiveHolder(ctx)
if err != nil {
    var pe *os.PathError
    if errors.As(err, &pe) && errors.Is(pe.Err, os.ErrPermission) {
        // run as same user as gate creator or fix permissions
        return fmt.Errorf("gate unreadable: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ExclusiveHolder when os.OpenFile(path, O_RDONLY) fails with e.g. EACCES (no read permission on an existing 0o600 gate file owned by another user), EISDIR (path is a directory), or ELOOP (symlink cycle).

Common situations: Probe run under a different uid than the Acquire process (0o600 file); the gate path was repointed at a directory; permissions changed after an earlier run created the gate.

Related errors


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