sipeed/picoclaw · error
credential: SSH private key is required but not found (set P
Error message
credential: SSH private key is required but not found (set PICOCLAW_SSH_KEY_PATH or place key at ~/.ssh/picoclaw_ed25519.key)
What it means
deriveKey requires a non-empty sshKeyPath because the AES key is derived from HMAC(SHA256(sshKeyBytes), passphrase). Encrypt/resolveEncrypted resolve the path via pickSSHKeyPath (explicit argument, then PICOCLAW_SSH_KEY_PATH, then auto-detected ~/.ssh/picoclaw_ed25519.key). This error means all three sources came up empty, so no KDF input beyond the passphrase exists and the library refuses to encrypt or decrypt.
Source
Thrown at pkg/credential/credential.go:289
// Within ~/.ssh/.
if userHome, err := os.UserHomeDir(); err == nil {
if isWithinDir(clean, filepath.Join(userHome, ".ssh")) {
return true
}
}
return false
}
// deriveKey derives a 32-byte AES-256 key from passphrase and SSH private key.
//
// ikm = HMAC-SHA256(key=SHA256(sshKeyBytes), msg=passphrase)
// Final key: HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
// sshKeyPath must be non-empty; returns an error otherwise.
func deriveKey(passphrase, sshKeyPath string, salt []byte) ([]byte, error) {
if sshKeyPath == "" {
return nil, fmt.Errorf(
"credential: SSH private key is required but not found" +
" (set PICOCLAW_SSH_KEY_PATH or place key at ~/.ssh/picoclaw_ed25519.key)")
}
if !allowedSSHKeyPath(sshKeyPath) {
return nil, fmt.Errorf(
"credential: SSH key path %q is not in an allowed location (PICOCLAW_SSH_KEY_PATH, PICOCLAW_HOME, or ~/.ssh/)",
sshKeyPath,
)
}
sshBytes, err := os.ReadFile(sshKeyPath)
if err != nil {
return nil, fmt.Errorf("credential: cannot read SSH key %q: %w", sshKeyPath, err)
}
sshHash := sha256.Sum256(sshBytes)
mac := hmac.New(sha256.New, sshHash[:])
mac.Write([]byte(passphrase))
ikm := mac.Sum(nil)
View on GitHub (pinned to 49183d7e8d)
Solutions
- Generate the key: run the picoclaw keygen command or call credential.GenerateSSHKey on the default path (creates ~/.ssh/picoclaw_ed25519.key with 0600)
- Or point to an existing key: export PICOCLAW_SSH_KEY_PATH=/path/to/private_key in the daemon's environment
- If the key was created under another account, copy it to the running user's ~/.ssh/picoclaw_ed25519.key (keep 0600)
- Check `echo $HOME` for the service user and that $HOME/.ssh is where you think it is
Example fix
// before: Encrypt on a machine with no key configured
enc, err := credential.Encrypt(passphrase, "", secret)
// after: bootstrap the key once, then encrypt
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
keyPath, err = credential.DefaultSSHKeyPath()
if err != nil {
return err
}
if err := credential.GenerateSSHKey(keyPath); err != nil {
return err
}
}
enc, err := credential.Encrypt(passphrase, keyPath, secret) Defensive patterns
Strategy: validation
Validate before calling
func ensureSSHKey() (string, error) {
if p := os.Getenv("PICOCLAW_SSH_KEY_PATH"); p != "" {
if _, err := os.Stat(p); err != nil {
return "", fmt.Errorf("configured key missing: %w", err)
}
return p, nil
}
p, err := credential.DefaultSSHKeyPath()
if err != nil {
return "", err
}
if _, err := os.Stat(p); os.IsNotExist(err) {
if err := credential.GenerateSSHKey(p); err != nil {
return "", err
}
}
return p, nil
} Try / catch
if _, err := credential.Encrypt(pass, keyPath, secret); err != nil {
if strings.Contains(err.Error(), "SSH private key is required") {
// bootstrap: generate the default key and retry once
if p, gerr := credential.DefaultSSHKeyPath(); gerr == nil {
if gerr = credential.GenerateSSHKey(p); gerr == nil {
_, err = credential.Encrypt(pass, p, secret)
}
}
}
if err != nil {
return err
}
} Prevention
- Run the keygen bootstrap during install/provisioning, before any credential is encrypted
- Set PICOCLAW_SSH_KEY_PATH explicitly in service definitions instead of relying on home-dir auto-detection
- Include the key in machine provisioning (config management) so fresh hosts are never keyless
- Do not export PICOCLAW_SSH_KEY_PATH as an empty string - pickSSHKeyPath treats set-but-empty as final
When it happens
Trigger: Calling Encrypt (or resolving an ENC- credential) when PICOCLAW_SSH_KEY_PATH is unset AND ~/.ssh/picoclaw_ed25519.key does not exist (findDefaultSSHKey's os.Stat fails). Also fires when PICOCLAW_SSH_KEY_PATH is explicitly exported as the empty string - pickSSHKeyPath honors the set-but-empty value and returns "".
Common situations: Fresh install where the keygen bootstrap step was never run; daemon running as root while the key was generated for a normal user (HOME points to /root); containers or CI images with no ~/.ssh; HOME unset so DefaultSSHKeyPath fails inside findDefaultSSHKey; key deleted after credentials were created.
Related errors
- credential: SSH key path %q is not in an allowed location (P
- credential: enc:// passphrase required
- credential: enc:// decryption failed (wrong passphrase or SS
- credential: enc:// invalid base64: %w
- credential: enc:// payload too short
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/4e926f4df713bffd.
Report an issue: GitHub.