JuliusBrussee/caveman · error

native session key chmod

Error message

native session key chmod: %w

What it means

The existing session.key passed the 32-byte size check but os.Chmod(path, 0600) failed. Almost always the file is owned by a different uid (a previous sudo run minted it), and chmod on a file you do not own is denied even inside a directory you control. The runtime enforces 0600 as a hard invariant for the shared HMAC key, so it refuses to continue with a group/world-readable key.

Solutions

  1. sudo chown $(id -u):$(id -g) <home>/runtime/session.key
  2. Or discard and re-mint: sudo rm <home>/runtime/session.key, then restart as the normal user
  3. Stop launching caveman under sudo; fix the launcher script
  4. lsattr <home>/runtime/session.key to rule out the immutable attribute
Defensive patterns

Strategy: validation

Validate before calling

keyPath := filepath.Join(home, "runtime", "session.key")
if info, err := os.Stat(keyPath); err == nil {
	if st, ok := info.Sys().(*syscall.Stat_t); ok && int(st.Uid) != os.Getuid() {
		return fmt.Errorf("%s owned by uid %d — run: sudo chown %d:%d %s (or remove it to re-mint)", keyPath, st.Uid, os.Getuid(), os.Getgid(), keyPath)
	}
}
// safe to call nativeruntime.LoadOrCreateSessionKey(home)

Type guard

func isPermissionErr(err error) bool {
	return errors.Is(err, fs.ErrPermission)
}

Try / catch

key, err := nativeruntime.LoadOrCreateSessionKey(home)
if err != nil {
	if errors.Is(err, fs.ErrPermission) && strings.Contains(err.Error(), "session key chmod") {
		log.Printf("key file is foreign-owned — sudo chown %d:%d %s or sudo rm it and restart", os.Getuid(), os.Getgid(), filepath.Join(home, "runtime", "session.key"))
	}
	return err
}

Prevention

When it happens

Trigger: session.key created under sudo/root and then caveman run as the normal user; the file was copied in by another user with ownership preserved; the immutable attribute was set on the file.

Common situations: The sudo-first-run pattern on shared machines; homes restored by an admin; files migrated between accounts with cp -p.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/2fa252f174668cd6. Report an issue: GitHub.

Appendix: source

Thrown at proxy/internal/nativeruntime/marker.go:64

			return nil, fmt.Errorf("native session key sync: %w", syncErr)
		}
		if closeErr := file.Close(); closeErr != nil {
			return nil, fmt.Errorf("native session key close: %w", closeErr)
		}
		return key, nil
	}
	if !errors.Is(err, os.ErrExist) {
		return nil, fmt.Errorf("native session key create: %w", err)
	}
	key, err = os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("native session key read: %w", err)
	}
	if len(key) != sessionKeyBytes {
		return nil, fmt.Errorf("native session key length = %d, want %d", len(key), sessionKeyBytes)
	}
	if err := os.Chmod(path, 0o600); err != nil {
		return nil, fmt.Errorf("native session key chmod: %w", err)
	}
	return key, nil
}

// SessionMarker builds model-temporary correlation context. Local proxy removes
// valid markers byte-surgically before provider inspection or forwarding.
func SessionMarker(key []byte, sessionID string) (string, error) {
	if len(key) != sessionKeyBytes || sessionID == "" || len(sessionID) > 256 {
		return "", errors.New("native session marker: invalid key or session id")
	}
	encoded := base64.RawURLEncoding.EncodeToString([]byte(sessionID))
	sig := markerMAC(key, encoded)
	return fmt.Sprintf(`[[caveman-session-v1 sid="%s" sig="%s"]]`, encoded, sig), nil
}

// StripSessionMarkers removes only valid HMAC-signed markers. Invalid marker-
// shaped user text remains byte-identical. Conflicting valid session IDs are
// stripped but return no correlation identity.

View on GitHub (pinned to 766dce6b13)