feat(doctor): audit config + state permissions

This commit is contained in:
Peter Steinberger 2026-01-08 21:51:34 +01:00
parent 13ddd40a59
commit 884e734809
5 changed files with 57 additions and 1 deletions

View File

@ -2,6 +2,7 @@
## Unreleased
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
- Daemon: align generated systemd unit with docs for network-online + restart delay. (#479) — thanks @azade-c
- Outbound: default Telegram account selection for config-only tokens; remove heartbeat-specific accountId handling. (follow-up #516) — thanks @YuriNachos

View File

@ -59,6 +59,7 @@ cat ~/.clawdbot/clawdbot.json
- Legacy config migration and normalization.
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
- State integrity and permissions checks (sessions, transcripts, state dir).
- Config file permission checks (chmod 600) when running locally.
- Legacy workspace dir detection (`~/clawdis`, `~/clawdbot`).
- Sandbox image repair when sandboxing is enabled.
- Legacy service migration and extra gateway detection.
@ -129,6 +130,8 @@ Doctor checks:
split between installs).
- **Remote mode reminder**: if `gateway.mode=remote`, doctor reminds you to run
it on the remote host (the state lives there).
- **Config file permissions**: warns if `~/.clawdbot/clawdbot.json` is
group/world readable and offers to tighten to `600`.
### 5) Sandbox image repair
When sandboxing is enabled, doctor checks Docker images and offers to build or

View File

@ -95,6 +95,14 @@ This is social engineering 101. Create distrust, encourage snooping.
## Configuration Hardening (examples)
### 0) File permissions
Keep config + state private on the gateway host:
- `~/.clawdbot/clawdbot.json`: `600` (user read/write only)
- `~/.clawdbot`: `700` (user only)
`clawdbot doctor` can warn and offer to tighten these permissions.
### 1) DMs: pairing by default
```json5

View File

@ -123,6 +123,7 @@ function findOtherStateDirs(stateDir: string): string[] {
export async function noteStateIntegrity(
cfg: ClawdbotConfig,
prompter: DoctorPrompterLike,
configPath?: string,
) {
const warnings: string[] = [];
const changes: string[] = [];
@ -186,6 +187,49 @@ export async function noteStateIntegrity(
}
}
}
if (stateDirExists && process.platform !== "win32") {
try {
const stat = fs.statSync(stateDir);
if ((stat.mode & 0o077) !== 0) {
warnings.push(
`- State directory permissions are too open (${stateDir}). Recommend chmod 700.`,
);
const tighten = await prompter.confirmSkipInNonInteractive({
message: `Tighten permissions on ${stateDir} to 700?`,
initialValue: true,
});
if (tighten) {
fs.chmodSync(stateDir, 0o700);
changes.push(`- Tightened permissions on ${stateDir} to 700`);
}
}
} catch (err) {
warnings.push(`- Failed to read ${stateDir} permissions: ${String(err)}`);
}
}
if (configPath && existsFile(configPath) && process.platform !== "win32") {
try {
const stat = fs.statSync(configPath);
if ((stat.mode & 0o077) !== 0) {
warnings.push(
`- Config file is group/world readable (${configPath}). Recommend chmod 600.`,
);
const tighten = await prompter.confirmSkipInNonInteractive({
message: `Tighten permissions on ${configPath} to 600?`,
initialValue: true,
});
if (tighten) {
fs.chmodSync(configPath, 0o600);
changes.push(`- Tightened permissions on ${configPath} to 600`);
}
}
} catch (err) {
warnings.push(
`- Failed to read config permissions (${configPath}): ${String(err)}`,
);
}
}
if (stateDirExists) {
const dirCandidates = new Map<string, string>();

View File

@ -146,7 +146,7 @@ export async function doctorCommand(
}
}
await noteStateIntegrity(cfg, prompter);
await noteStateIntegrity(cfg, prompter, snapshot.path ?? CONFIG_PATH_CLAWDBOT);
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
noteSandboxScopeWarnings(cfg);