siyuan-note/siyuan · error
stat encrypted notebook history backup [%s] failed: %w
Error message
stat encrypted notebook history backup [%s] failed: %w
What it means
isEncryptedHistoryBoxDir decides whether a history box dir is an encrypted notebook's box by probing .siyuan/notebook-crypto-backup.json. A failed os.Stat that is not NotExist is wrapped as 'stat encrypted notebook history backup [%s] failed'. The library throws it because an inaccessible backup marker must abort classification rather than classify the box as unencrypted (which could authorize destructive operations on recovery material).
Source
Thrown at kernel/model/crypto.go:939
func HasEncryptedNotebookHistory() bool {
hasHistory, err := scanEncryptedNotebookHistory()
if err != nil {
logging.LogErrorf("scan encrypted notebook history failed: %s", err)
return true
}
return hasHistory
}
// isEncryptedHistoryBoxDir 判断历史目录中的 boxID 子目录是否属于加密笔记本。
// 优先看 notebook-crypto-backup.json(删除前随 box 目录整体备份,是加密身份的权威标识),
// 再 fallback 到 conf.json 的 Encrypted 标志。
func isEncryptedHistoryBoxDir(boxDir string) (bool, error) {
siyuanDir := filepath.Join(boxDir, ".siyuan")
backupPath := filepath.Join(siyuanDir, "notebook-crypto-backup.json")
if _, err := os.Stat(backupPath); err == nil {
return true, nil
} else if !os.IsNotExist(err) {
return false, fmt.Errorf("stat encrypted notebook history backup [%s] failed: %w", boxDir, err)
}
confPath := filepath.Join(siyuanDir, "conf.json")
if _, err := os.Stat(confPath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("stat encrypted notebook history conf [%s] failed: %w", boxDir, err)
}
data, err := filelock.ReadFile(confPath)
if err != nil {
return false, fmt.Errorf("read encrypted notebook history conf [%s] failed: %w", boxDir, err)
}
var boxConf conf.BoxConf
if err = gulu.JSON.UnmarshalJSON(data, &boxConf); err != nil {
return false, fmt.Errorf("parse encrypted notebook history conf [%s] failed: %w", boxDir, err)
}
return boxConf.Encrypted, nil
}View on GitHub (pinned to 8641553a1f)
Solutions
- Read the wrapped OS error and fix access to <boxDir>/.siyuan/notebook-crypto-backup.json (chmod/chown or ACLs)
- Verify .siyuan is still a directory and not a file/symlink; restore it from backup if broken
- Re-mount or repair the storage if the error is I/O level (ESTALE, EIO)
- Retry the scan after fixing; do not delete or rewrite history boxes while the error persists
Example fix
// before: unreadable .siyuan in snapshot box ls -l data/history/20260901-delete/20240101120000-abc123/ # .siyuan root:root 000 // after sudo chmod -R u+rwX data/history/20260901-delete/20240101120000-abc123/.siyuan sudo chown -R $USER data/history/20260901-delete/20240101120000-abc123/.siyuan
Defensive patterns
Strategy: try-catch
Validate before calling
p := filepath.Join(boxDir, ".siyuan", "notebook-crypto-backup.json")
if _, err := os.Stat(p); err != nil && !os.IsNotExist(err) { return err } Type guard
func backupStatable(boxDir string) bool { _, err := os.Stat(filepath.Join(boxDir, ".siyuan", "notebook-crypto-backup.json")); return err == nil || os.IsNotExist(err) } Try / catch
enc, err := isEncryptedHistoryBoxDir(boxDir)
if err != nil {
// unknown status: treat as encrypted to protect recovery material
handleAsEncrypted(boxDir)
return
} Prevention
- Restore snapshots with the same owner/permissions as the kernel process
- Keep .siyuan a directory — never overwrite it with a file
- Repair ACLs after cross-machine workspace moves (macOS/Windows)
- Treat stat errors on history boxes as 'unknown, assume encrypted'
When it happens
Trigger: encryptedNotebookHistoryBoxDirs, hasEncryptedNotebookDeleteHistory, or IsEncryptedHistoryPath statting the backup file when permission is denied on the box dir or its .siyuan subdir, the path component is not a directory, or the filesystem returns an I/O error (e.g. EXDEV/ESTALE on network mounts).
Common situations: Snapshots restored with root ownership; .siyuan replaced by a file; corrupted network-share mounts; macOS/Windows ACLs blocking the kernel process after workspace moves between machines.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- stat encrypted notebook history conf [%s] failed: %w
- check encrypted notebook history failed: %w
- get history directory failed: %w
- read session file failed: %w
- create session dir failed: %w
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/a6b6d96a35dc56c0.
Report an issue: GitHub.