URL Protocol Reference
Three schemes are registered as protocol clients. All of them reach the app through a single open-url handler in main.js, which recognises 19 command names and forwards each to one IPC channel. Seven of those commands reach the app. Three type a request into the chat without carrying it out. Nine do nothing at all.
Delivery is macOS only
The schemes are registered on every platform, but the handler is an app.on('open-url') event, which Electron only emits on macOS. Windows and Linux deliver a protocol activation as an argv element on a second process launch, and this app registers neither requestSingleInstanceLock nor a second-instance handler, and never readsprocess.argv for a URL. On those platforms the link opens the app and is discarded.
URL Schemes
Registered Schemes
aartiq://Command links, also used by Raycast and macOS Shortcuts
main.js:open-url accepts aartiq:// and comet://. comet:// is handled at runtime but is not in package.json, so nothing registers it.
aartiq-browser://OAuth callback
The whole URL is forwarded to the renderer on the auth-callback channel, which preload.js subscribes to.
http:// and https://Default-browser registration
Forwarded to add-new-tab, so a link opened through the protocol client loads as a browser tab.
Command Reference
aartiq:// Commands
aartiq://chat?message | prompt | queryReaches the appOpens the AI sidebar and sends the text to ai-chat-input-text, which fills the chat composer. The text is not submitted.
aartiq://ask-ai?message | prompt | queryReaches the appIdentical to chat.
aartiq://voice-chatReaches the appOpens the AI sidebar. Does not start voice input.
aartiq://navigate?urlReaches the appSends navigate-to-url. A bare host is prefixed with https://. This is one of the few sends that a renderer channel actually listens for.
aartiq://search?query | promptReaches the appOpens a new tab on https://www.google.com/search?q=... The engine is hardcoded; there is no setting for it.
aartiq://volume?level (0-100)Reaches the appRuns osascript to set the output volume. macOS only, and it bypasses the approval gate — it is a raw shell call in the main process.
aartiq://open-app?appNameReaches the appRuns open -a with the name. macOS only, and also a raw shell call.
aartiq://create-pdf?title, contentTypes a request into chat, does not run itOpens chat and types: Create a PDF titled "<title>" with this content: ... Returns success. No PDF is created and nothing is generated.
aartiq://create-doc?title, contentNothing happensThe string create-doc does not appear in the action handler at all. It is in the command map, pointing at the ai:create-pdf channel, but there is no branch for it, so the handler answers "Unknown or unsupported action: create-doc" and the channel send goes nowhere. It does not share a branch with create-pdf.
aartiq://run-command?commandTypes a request into chat, does not run itOpens chat and types: Run this shell command: <command>. Returns success with the message "Prepared shell command request". No process is started. Note that the command text arrives as chat input, so it is not an execution path and is not subject to shell approval gating.
aartiq://schedule?task, cron | scheduleTypes a request into chat, does not run itOpens chat and types: Schedule this task: <task>. Returns success. No automation is created and the cron string is never parsed.
aartiq://screenshotNothing happensRouted to executeShortcutAction, which has no branch for it, so it returns "Unknown or unsupported action". The channel it is then sent to has no listener.
aartiq://set-modelNothing happensSame as screenshot: no branch, no listener.
aartiq://browseNothing happensNot in the action list at all, so the only thing that happens is a send on a channel with no listener.
aartiq://ocrNothing happensSame as browse.
aartiq://pdfNothing happensSame as browse.
aartiq://automationNothing happensSame as browse.
aartiq://settingsNothing happensSame as browse.
aartiq://indexNothing happensSame as browse. It is also the fallback for an unrecognised command name.
Query Parameters
Parameters Actually Read
| Parameter | Read by |
|---|---|
| message / prompt / query | chat, ask-ai, search |
| url | navigate |
| content, title | create-pdf |
| command | run-command |
| task, cron / schedule | schedule |
| level | volume |
| appName | open-app |
| speak | sent on a channel with no listener — see below |
Every other key in the query string is parsed into the params object and then ignored. There is no theme, language, settings-tab or file parameter, and adding one will not do anything.
Implementation
The Code
main.js — open-url
// main.js — the whole deep-link surface
app.on('open-url', async (event, url) => {
event.preventDefault();
let target = getTopWindow();
if (!target) {
await createWindow();
target = mainWindow;
}
if (!target || target.isDestroyed()) return;
const parsed = new URL(url);
const pathname = parsed.pathname.replace(/^\/+/, '');
const params = Object.fromEntries(parsed.searchParams);
if (url.startsWith('aartiq-browser://')) {
target.webContents.send('auth-callback', url);
} else if (url.startsWith('http://') || url.startsWith('https://')) {
target.webContents.send('add-new-tab', url);
} else if (url.startsWith('aartiq://') || url.startsWith('comet://')) {
const command = parsed.hostname || pathname || params.command || 'index';
const commandMap = {
'chat': 'open-ai-chat',
'search': 'ai:search',
'navigate': 'navigate-to-url',
'create-pdf': 'ai:create-pdf',
'create-doc': 'ai:create-pdf',
'run-command': 'shell:execute',
'open-app': 'system:open-app',
'screenshot': 'system:screenshot',
'volume': 'system:set-volume',
'schedule': 'ai:schedule',
'ask-ai': 'ai:ask-speaking',
'voice-chat': 'ai:voice-chat',
'set-model': 'ai:set-model',
'browse': 'open-quick-browse',
'ocr': 'trigger-screen-ocr',
'pdf': 'open-pdf-creator',
'automation': 'open-automation-panel',
'settings': 'open-settings',
'index': 'open-main',
};
const siriActions = ['chat', 'search', 'navigate', 'create-pdf', 'create-doc',
'run-command', 'open-app', 'screenshot', 'volume', 'schedule', 'ask-ai',
'voice-chat', 'set-model'];
if (siriActions.includes(command)) {
executeShortcutAction(command, params);
if (params.speak === 'true') {
target.webContents.once('did-finish-load', () => {
target.webContents.send('ai:request-speak-response', params);
});
}
}
// Sent unconditionally. Of these nineteen channel names only
// navigate-to-url has a renderer listener; the rest are discarded.
target.webContents.send(commandMap[command] || command, params);
}
if (target.isMinimized()) target.restore();
target.focus();
});Reaches the app
chat, ask-ai, voice-chat, navigate, search, volume, open-app. chat, ask-ai, create-pdf, run-command and schedule go through the ai-chat-input-text channel, which the preload does subscribe to.
Returns success without doing the work
create-pdf, run-command and schedule. Each returns success: true while only having typed a sentence into the chat composer. A caller that checks the return value will believe a PDF was created or a task was scheduled.
Nothing happens
browse, ocr, pdf, automation, settings, index, screenshot, set-model and create-doc have no branch in the action handler — create-doc despite being pointed at the create-pdf channel, and screenshot and set-model despite both being in the action list. All nine end in a send on a channel with no listener.
Edge Cases
Worth Knowing
Unrecognised commands
The command is read from parsed.hostname || pathname, with index as the fallback. An unknown command falls back to index and then sends the raw command name as the channel, so the failure is silent.
aartiq://approve is handled elsewhere
High-risk MCP tool calls render a QR code containing aartiq://approve?id=…&pin=…. That URL is meant to be opened on the phone, and the Flutter app is what consumes it. On the desktop the same URL matches the command branch above and is discarded. The token and PIN are verified over HTTP by the MCP server, not by the deep-link handler.
volume and open-app skip the approval gate
Both call execPromise directly from the main process with an interpolated argument. They are not routed through the capability controller that gates execute-shell-command. They are also the only two commands that cannot work off macOS.
?speak=true does nothing
It queues a send on ai:request-speak-response behind did-finish-load. No listener for that channel exists anywhere in the codebase, so the reply is never spoken.
Claims removed from this page
- —In-app route table. There is no /chat, /automation, /settings, /docs or /pdf-viewer routing target. The handler matches command names, not paths, and ignores anything it does not recognise.
- —?tab=, ?theme=, ?lang= parameters. They are parsed and then never read.
- —The Flutter example, which was JavaScript and Objective-C syntax presented as Dart, called a DeepLinkHandler class that does not exist, and used navigator.pushNamed on a context that was never in scope.
- —Firebase Dynamic Links, a Google service retired in 2025 and not present in the codebase.
- —apple-app-site-association and assetlinks.json for a domain. Nothing in the app fetches or verifies either file.
- —An iOS Info.plist snippet. There is no iOS app; the mobile client is Flutter on Android and iOS, and its scheme handling is not this snippet.
- —Windows Jump List and App Links. Neither is implemented.
- —The web origin as a way to open in-app routes. A web link either loads a real page in a tab or, if the origin does not serve one, nothing.