{"record":{"id":"315a9e72204788e9","repo":"JuliusBrussee/caveman","slug":"native-session-key-mkdir-w","errorCode":null,"errorMessage":"native session key mkdir: %w","messagePattern":"native session key mkdir: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/internal/nativeruntime/marker.go","lineNumber":26,"sourceCode":"\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n)\n\nconst sessionKeyBytes = 32\n\nvar markerPattern = regexp.MustCompile(`\\[\\[caveman-session-v1 sid=\"([A-Za-z0-9_-]{1,384})\" sig=\"([0-9a-f]{64})\"\\]\\]`)\n\n// LoadOrCreateSessionKey returns one user-only HMAC key shared by CLI adapters\n// and local proxy. O_EXCL makes concurrent first startup converge on one key.\nfunc LoadOrCreateSessionKey(home string) ([]byte, error) {\n\tdir := filepath.Join(home, \"runtime\")\n\tif err := os.MkdirAll(dir, 0o700); err != nil {\n\t\treturn nil, fmt.Errorf(\"native session key mkdir: %w\", err)\n\t}\n\tif err := os.Chmod(dir, 0o700); err != nil {\n\t\treturn nil, fmt.Errorf(\"native session key chmod dir: %w\", err)\n\t}\n\tpath := filepath.Join(dir, \"session.key\")\n\tkey := make([]byte, sessionKeyBytes)\n\tif _, err := rand.Read(key); err != nil {\n\t\treturn nil, fmt.Errorf(\"native session key random: %w\", err)\n\t}\n\tfile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)\n\tif err == nil {\n\t\tif _, writeErr := file.Write(key); writeErr != nil {\n\t\t\t_ = file.Close()\n\t\t\t_ = os.Remove(path)\n\t\t\treturn nil, fmt.Errorf(\"native session key write: %w\", writeErr)\n\t\t}\n\t\tif syncErr := file.Sync(); syncErr != nil {\n\t\t\t_ = file.Close()","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/proxy/internal/nativeruntime/marker.go#L8-L44","documentation":"Returned by nativeruntime.LoadOrCreateSessionKey when os.MkdirAll cannot create the runtime directory under the supplied home; the HMAC session key lives at <home>/runtime/session.key. The wrapped error is the raw *fs.PathError from MkdirAll, and the underlying errno (usually EACCES, EEXIST-on-a-regular-file, EROFS, or ENAMETOOLONG) tells the real story. Startup aborts before any key is minted.","triggerScenarios":"Calling LoadOrCreateSessionKey(home) when <home> or a parent directory is owned by another user (classic: caveman was run once under sudo, then as the normal user); <home>/runtime already exists as a regular file; home sits on a read-only container layer or a full volume; HOME unset so home resolves to an unwritable path such as /.","commonSituations":"sudo-first-run leaving a root-owned ~/.caveman; CI containers with a read-only root filesystem; CAVEMAN_HOME pointing at a mount that vanished or is quota-exceeded; portable installs on media mounted read-only.","solutions":["Run ls -ld <home> <home>/runtime and fix ownership: sudo chown -R $(id -u):$(id -g) <home>","If runtime exists as a regular file, remove it: rm <home>/runtime","Point CAVEMAN_HOME (or HOME) at a writable directory on a local filesystem","In containers, mount a writable volume over the caveman home instead of using the image layer"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"func homeIsWritableByCurrentUser(home string) error {\n\tinfo, err := os.Stat(home)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"home missing: %w\", err)\n\t}\n\tif st, ok := info.Sys().(*syscall.Stat_t); ok && int(st.Uid) != os.Getuid() {\n\t\treturn fmt.Errorf(\"%s owned by uid %d, not %d — run: sudo chown -R %d:%d %s\", home, st.Uid, os.Getuid(), os.Getuid(), os.Getgid(), home)\n\t}\n\tprobe := filepath.Join(home, \".write-probe\")\n\tif err := os.WriteFile(probe, nil, 0o600); err != nil {\n\t\treturn fmt.Errorf(\"home not writable: %w\", err)\n\t}\n\treturn os.Remove(probe)\n}\n\n// before nativeruntime.LoadOrCreateSessionKey(home)\nif err := homeIsWritableByCurrentUser(home); err != nil {\n\treturn err\n}","typeGuard":"func isPermissionErr(err error) bool {\n\treturn errors.Is(err, fs.ErrPermission)\n}","tryCatchPattern":"key, err := nativeruntime.LoadOrCreateSessionKey(home)\nif err != nil {\n\tif errors.Is(err, fs.ErrPermission) {\n\t\tlog.Printf(\"fix ownership first: sudo chown -R %d:%d %s\", os.Getuid(), os.Getgid(), home)\n\t}\n\treturn fmt.Errorf(\"session key startup: %w\", err)\n}","preventionTips":["Never launch caveman components under sudo or root cron — mixed uids create the foreign-owned home that produces EACCES","Pin CAVEMAN_HOME to one writable local path per user and export it in launcher scripts","Add a startup self-test that stats the home and compares uid before calling LoadOrCreateSessionKey","In containers, mount a writable volume at the caveman home instead of relying on the image layer"],"tags":["go","filesystem","permissions","startup"],"backgroundTag":"mkdir-permission-denied","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}