diff --git a/CHANGELOG.md b/CHANGELOG.md
index 616b5d7e315..c82d27c5e8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -143,6 +143,7 @@ Docs: https://docs.openclaw.ai
- Plugins/Install: clear stale install errors when an npm package is not found so follow-up install attempts report current state correctly. (#25073) Thanks @dalefrieswthat.
- Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.
- Gateway/macOS supervised restart: actively `launchctl kickstart -k` during intentional supervised restarts to bypass LaunchAgent `ThrottleInterval` delays, and fall back to in-process restart when kickstart fails. Landed from contributor PR #29078 by @cathrynlavery. Thanks @cathrynlavery.
+- Gateway/macOS LaunchAgent hardening: write `Umask=077` in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.
- Daemon/macOS TLS certs: default LaunchAgent service env `NODE_EXTRA_CA_CERTS` to `/etc/ssl/cert.pem` (while preserving explicit overrides) so HTTPS clients no longer fail with local-issuer errors under launchd. (#27915) Thanks @Lukavyi.
- Discord/Components wildcard handlers: use distinct internal registration sentinel IDs and parse those sentinels as wildcard keys so select/user/role/channel/mentionable/modal interactions are not dropped by raw customId dedupe paths. Landed from contributor PR #29459 by @Sid-Qin. Thanks @Sid-Qin.
- Feishu/Reaction notifications: add `channels.feishu.reactionNotifications` (`off | own | all`, default `own`) so operators can disable reaction ingress or allow all verified reaction events (not only bot-authored message reactions). (#28529) Thanks @cowboy129.
diff --git a/src/daemon/launchd-plist.ts b/src/daemon/launchd-plist.ts
index 37448cdcebf..8cca6d6d8e0 100644
--- a/src/daemon/launchd-plist.ts
+++ b/src/daemon/launchd-plist.ts
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
// intentional gateway restarts. Keep it low so CLI restarts and forced
// reinstalls do not stall for a full minute.
export const LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 1;
+export const LAUNCH_AGENT_UMASK_DECIMAL = 0o077;
const plistEscape = (value: string): string =>
value
@@ -111,5 +112,5 @@ export function buildLaunchAgentPlist({
? `\n Comment\n ${plistEscape(comment.trim())}`
: "";
const envXml = renderEnvDict(environment);
- return `\n\n\n \n Label\n ${plistEscape(label)}\n ${commentXml}\n RunAtLoad\n \n KeepAlive\n \n ThrottleInterval\n ${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}\n ProgramArguments\n ${argsXml}\n \n ${workingDirXml}\n StandardOutPath\n ${plistEscape(stdoutPath)}\n StandardErrorPath\n ${plistEscape(stderrPath)}${envXml}\n \n\n`;
+ return `\n\n\n \n Label\n ${plistEscape(label)}\n ${commentXml}\n RunAtLoad\n \n KeepAlive\n \n ThrottleInterval\n ${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}\n Umask\n ${LAUNCH_AGENT_UMASK_DECIMAL}\n ProgramArguments\n ${argsXml}\n \n ${workingDirXml}\n StandardOutPath\n ${plistEscape(stdoutPath)}\n StandardErrorPath\n ${plistEscape(stderrPath)}${envXml}\n \n\n`;
}
diff --git a/src/daemon/launchd.test.ts b/src/daemon/launchd.test.ts
index 6cf31dc5ce5..0266038d0b9 100644
--- a/src/daemon/launchd.test.ts
+++ b/src/daemon/launchd.test.ts
@@ -1,6 +1,9 @@
import { PassThrough } from "node:stream";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS } from "./launchd-plist.js";
+import {
+ LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS,
+ LAUNCH_AGENT_UMASK_DECIMAL,
+} from "./launchd-plist.js";
import {
installLaunchAgent,
isLaunchAgentListed,
@@ -201,6 +204,8 @@ describe("launchd install", () => {
expect(plist).not.toContain("SuccessfulExit");
expect(plist).toContain("ThrottleInterval");
expect(plist).toContain(`${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}`);
+ expect(plist).toContain("Umask");
+ expect(plist).toContain(`${LAUNCH_AGENT_UMASK_DECIMAL}`);
});
it("restarts LaunchAgent with bootout-bootstrap-kickstart order", async () => {