diff --git a/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift
index 079fdc8e7..7be78c407 100644
--- a/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift
+++ b/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift
@@ -26,15 +26,24 @@ actor GatewayEndpointStore {
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] },
password: {
- // First check environment variable
let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PASSWORD"] ?? ""
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
return trimmed
}
- // Then check config file based on connection mode
let root = ClawdisConfigFile.loadDict()
- // Check gateway.auth.password (for local gateway auth)
+ if CommandResolver.connectionModeIsRemote() {
+ if let gateway = root["gateway"] as? [String: Any],
+ let remote = gateway["remote"] as? [String: Any],
+ let password = remote["password"] as? String
+ {
+ let pw = password.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !pw.isEmpty {
+ return pw
+ }
+ }
+ return nil
+ }
if let gateway = root["gateway"] as? [String: Any],
let auth = gateway["auth"] as? [String: Any],
let password = auth["password"] as? String
@@ -44,16 +53,6 @@ actor GatewayEndpointStore {
return pw
}
}
- // Check gateway.remote.password (for remote gateway auth)
- if let gateway = root["gateway"] as? [String: Any],
- let remote = gateway["remote"] as? [String: Any],
- let password = remote["password"] as? String
- {
- let pw = password.trimmingCharacters(in: .whitespacesAndNewlines)
- if !pw.isEmpty {
- return pw
- }
- }
return nil
},
localPort: { GatewayEnvironment.gatewayPort() },
diff --git a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift
index 9cc01ffef..7d0f04995 100644
--- a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift
+++ b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift
@@ -72,15 +72,17 @@ enum GatewayLaunchAgentManager {
sips
"""
if let token {
+ let escapedToken = self.escapePlistValue(token)
envEntries += """
CLAWDIS_GATEWAY_TOKEN
- \(token)
+ \(escapedToken)
"""
}
if let password {
+ let escapedPassword = self.escapePlistValue(password)
envEntries += """
CLAWDIS_GATEWAY_PASSWORD
- \(password)
+ \(escapedPassword)
"""
}
let plist = """
@@ -171,6 +173,15 @@ enum GatewayLaunchAgentManager {
return nil
}
+ private static func escapePlistValue(_ raw: String) -> String {
+ raw
+ .replacingOccurrences(of: "&", with: "&")
+ .replacingOccurrences(of: "<", with: "<")
+ .replacingOccurrences(of: ">", with: ">")
+ .replacingOccurrences(of: "\"", with: """)
+ .replacingOccurrences(of: "'", with: "'")
+ }
+
private struct LaunchctlResult {
let status: Int32
let output: String
diff --git a/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift b/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift
index 64f117ec2..6061fc8f9 100644
--- a/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift
+++ b/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift
@@ -29,6 +29,7 @@ import Testing
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { "t" },
+ password: { nil },
localPort: { 1234 },
remotePortIfRunning: { nil },
ensureRemoteTunnel: { 18789 }))
@@ -44,6 +45,7 @@ import Testing
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { nil },
+ password: { nil },
localPort: { 18789 },
remotePortIfRunning: { nil },
ensureRemoteTunnel: { 18789 }))
@@ -58,6 +60,7 @@ import Testing
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { "tok" },
+ password: { "pw" },
localPort: { 1 },
remotePortIfRunning: { 5555 },
ensureRemoteTunnel: { 5555 }))
@@ -69,13 +72,14 @@ import Testing
_ = try await store.ensureRemoteControlTunnel()
let next = await iterator.next()
- guard case let .ready(mode, url, token) = next else {
+ guard case let .ready(mode, url, token, password) = next else {
Issue.record("expected .ready after ensure, got \(String(describing: next))")
return
}
#expect(mode == .remote)
#expect(url.absoluteString == "ws://127.0.0.1:5555")
#expect(token == "tok")
+ #expect(password == "pw")
}
@Test func unconfiguredModeRejectsConfig() async {
@@ -83,6 +87,7 @@ import Testing
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { nil },
+ password: { nil },
localPort: { 18789 },
remotePortIfRunning: { nil },
ensureRemoteTunnel: { 18789 }))
diff --git a/src/gateway/call.ts b/src/gateway/call.ts
index e17f8203f..edecfa285 100644
--- a/src/gateway/call.ts
+++ b/src/gateway/call.ts
@@ -46,7 +46,7 @@ export async function callGateway(
(typeof opts.password === "string" && opts.password.trim().length > 0
? opts.password.trim()
: undefined) ||
- (process.env.CLAWDIS_GATEWAY_PASSWORD?.trim()) ||
+ process.env.CLAWDIS_GATEWAY_PASSWORD?.trim() ||
(typeof remote?.password === "string" && remote.password.trim().length > 0
? remote.password.trim()
: undefined);
diff --git a/src/gateway/hooks-mapping.ts b/src/gateway/hooks-mapping.ts
index 81c001878..0b2ee8132 100644
--- a/src/gateway/hooks-mapping.ts
+++ b/src/gateway/hooks-mapping.ts
@@ -18,7 +18,13 @@ export type HookMappingResolved = {
messageTemplate?: string;
textTemplate?: string;
deliver?: boolean;
- channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
+ channel?:
+ | "last"
+ | "whatsapp"
+ | "telegram"
+ | "discord"
+ | "signal"
+ | "imessage";
to?: string;
thinking?: string;
timeoutSeconds?: number;
@@ -50,7 +56,13 @@ export type HookAction =
wakeMode: "now" | "next-heartbeat";
sessionKey?: string;
deliver?: boolean;
- channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
+ channel?:
+ | "last"
+ | "whatsapp"
+ | "telegram"
+ | "discord"
+ | "signal"
+ | "imessage";
to?: string;
thinking?: string;
timeoutSeconds?: number;
@@ -86,7 +98,7 @@ type HookTransformResult = Partial<{
name: string;
sessionKey: string;
deliver: boolean;
- channel: "last" | "whatsapp" | "telegram" | "discord";
+ channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
to: string;
thinking: string;
timeoutSeconds: number;