sipeed/picoclaw · error

credential: keygen: write private key %q: %w

Error message

credential: keygen: write private key %q: %w

What it means

GenerateSSHKey writes the OpenSSH PEM private key with os.WriteFile(path, privPEM, 0600) and wraps failures. Common causes: permission denied on the directory, disk full (ENOSPC/EDQUOT), a directory existing at the key path, or an immutable existing file. Because the private key is written before the .pub file, a failure here leaves nothing behind (the pub write at line 58 never happens).

Source

Thrown at pkg/credential/keygen.go:46

func GenerateSSHKey(path string) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		return fmt.Errorf("credential: keygen: cannot create directory %q: %w", filepath.Dir(path), err)
	}

	pubRaw, privRaw, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return fmt.Errorf("credential: keygen: ed25519 key generation failed: %w", err)
	}

	// Marshal private key as OpenSSH PEM.
	block, err := ssh.MarshalPrivateKey(privRaw, "")
	if err != nil {
		return fmt.Errorf("credential: keygen: marshal private key: %w", err)
	}
	privPEM := pem.EncodeToMemory(block)

	if err = os.WriteFile(path, privPEM, 0o600); err != nil {
		return fmt.Errorf("credential: keygen: write private key %q: %w", path, err)
	}

	// Marshal public key as authorized_keys line.
	sshPub, err := ssh.NewPublicKey(pubRaw)
	if err != nil {
		return fmt.Errorf("credential: keygen: marshal public key: %w", err)
	}
	pubLine := ssh.MarshalAuthorizedKey(sshPub)

	pubPath := path + ".pub"
	if err := os.WriteFile(pubPath, pubLine, 0o644); err != nil {
		return fmt.Errorf("credential: keygen: write public key %q: %w", pubPath, err)
	}

	return nil
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check writability and space: `ls -ld <dir>`, `df -h <dir>`, then free space or fix ownership
  2. Ensure no directory occupies the key path: `ls -ld <path>`
  3. If an immutable/locked file exists, remove it first (`chattr -i` on Linux if applicable)
  4. Re-run the keygen after fixing the filesystem issue - the function overwrites existing files cleanly

Example fix

// before
if err := credential.GenerateSSHKey(path); err != nil {
    return err
}

// after: pre-flight the exact failure modes
if fi, err := os.Stat(path); err == nil && fi.IsDir() {
    return fmt.Errorf("%s is a directory", path)
}
dir := filepath.Dir(path)
if err := unix.Access(dir, unix.W_OK); err != nil {
    return fmt.Errorf("no write access to %s: %w", dir, err)
}
if err := credential.GenerateSSHKey(path); err != nil {
    return err
}
Defensive patterns

Strategy: validation

Validate before calling

func keyPathWritable(path string) error {
    if fi, err := os.Stat(path); err == nil && fi.IsDir() {
        return fmt.Errorf("%s is a directory", path)
    }
    dir := filepath.Dir(path)
    f, err := os.OpenFile(filepath.Join(dir, ".keygen-probe"), os.O_CREATE|os.O_WRONLY, 0o600)
    if err != nil {
        return fmt.Errorf("directory %s not writable: %w", dir, err)
    }
    f.Close()
    os.Remove(filepath.Join(dir, ".keygen-probe"))
    return nil
}

Prevention

When it happens

Trigger: GenerateSSHKey where the parent dir is not writable by the current user (dir was created 0700 by another uid); /tmp full in ephemeral CI; path is an existing directory; previous key file has the immutable bit set.

Common situations: Keygen as root after dir creation as user (or vice versa); small disks / quota-limited home directories; containers with size-limited tmpfs; leftover directory named picoclaw_ed25519.key from a failed run.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/eb3bba3289d060f1. Report an issue: GitHub.