gastownhall/beads · error

identity: write proxy secret: %w

Error message

identity: write proxy secret: %w

What it means

WriteSecret writes the generated hex secret to <rootDir>/proxy.secret using atomicfile.WriteFile with 0600 permissions. This error wraps any failure from that atomic write: inability to create the root directory path, permission denied, read-only filesystem, disk full, or a failure in the atomic rename/replace step. The library throws it because the control-listener secret could not be persisted for later readers.

Source

Thrown at internal/storage/dbproxy/identity/identity.go:49

	}
	resolved, err := filepath.EvalSymlinks(abs)
	if err != nil {
		return "", fmt.Errorf("identity: resolve root path: %w", err)
	}
	sum := sha256.Sum256([]byte(resolved))
	return hex.EncodeToString(sum[:]), nil
}

// WriteSecret creates and atomically writes a new control-listener secret.
// Each proxy start intentionally rotates the previous secret.
func WriteSecret(rootDir string) (string, error) {
	raw := make([]byte, 32)
	if _, err := rand.Read(raw); err != nil {
		return "", fmt.Errorf("identity: generate proxy secret: %w", err)
	}
	secret := hex.EncodeToString(raw)
	if err := atomicfile.WriteFile(filepath.Join(rootDir, SecretFileName), []byte(secret+"\n"), 0o600); err != nil {
		return "", fmt.Errorf("identity: write proxy secret: %w", err)
	}
	return secret, nil
}

// ReadSecret reads and validates the control-listener secret.
func ReadSecret(rootDir string) (string, error) {
	data, err := os.ReadFile(filepath.Join(rootDir, SecretFileName)) // #nosec G304 - rootDir is the workspace proxy root, not user input
	if err != nil {
		return "", fmt.Errorf("identity: read proxy secret: %w", err)
	}
	secret := strings.TrimSpace(string(data))
	if len(secret) != 64 {
		return "", errors.New("identity: invalid proxy secret")
	}
	if _, err := hex.DecodeString(secret); err != nil {
		return "", errors.New("identity: invalid proxy secret")
	}
	return secret, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure rootDir exists and is writable: os.MkdirAll(rootDir, 0o755) and check ownership
  2. Check disk space (df) and that the filesystem is mounted read-write
  3. Fix file/directory permissions on <rootDir>/proxy.secret (chown/chmod) to match the running user
  4. Read the wrapped error (errors.Unwrap) for the exact OS cause (fs.ErrNotExist, fs.ErrPermission, etc.)

Example fix

// before
secret, err := identity.WriteSecret(rootDir) // fails if rootDir missing
// after
if err := os.MkdirAll(rootDir, 0o755); err != nil {
    return err
}
secret, err := identity.WriteSecret(rootDir)
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(rootDir)
if err != nil {
    return fmt.Errorf("root %q missing: %w", rootDir, err)
}
if !info.IsDir() {
    return fmt.Errorf("%q is not a directory", rootDir)
}
if err := unix.Access(rootDir, unix.W_OK); err != nil {
    return fmt.Errorf("root %q not writable: %w", rootDir, err)
}

Try / catch

secret, err := identity.WriteSecret(rootDir)
if err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) && errors.Is(perr.Err, fs.ErrPermission) {
        // fix ownership/permissions on rootDir or proxy.secret
    }
    return err
}

Prevention

When it happens

Trigger: Calling identity.WriteSecret(rootDir) when rootDir does not exist or is not writable, when proxy.secret exists but is not writable/replaceable by the current user, when the filesystem is read-only or full, or when atomicfile's temp-file + rename sequence fails (e.g. cross-device issue or locked directory).

Common situations: Running bd as a different user than the one who owns the workspace (stale permissions), running with a read-only mount or full disk, pointing at a rootDir path that was never created, or SELinux/AppArmor blocking writes to the directory.

Related errors


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