# Deep Linking URL Scheme Handler Implementation in Craft Agents OSS

> Implement deep linking with the craftagents:// URL scheme in Craft Agents OSS. Parse links, route commands, and render views efficiently.

- Repository: [Craft Docs/craft-agents-oss](https://github.com/lukilabs/craft-agents-oss)
- Tags: deep-dive
- Published: 2026-04-18

---

**Craft Agents OSS implements deep linking through a custom `craftagents://` URL scheme that parses incoming links in the server-core RPC layer, routes navigation commands via inter-process communication, and renders the appropriate workspace or view in the Electron main process.**

Craft Agents OSS (lukilabs/craft-agents-oss) uses a custom `craftagents://` protocol to enable deep linking from external applications and web browsers into specific workspaces, views, or actions within the Electron application. The deep linking URL scheme handler implementation spans three architectural layers: URL parsing in the server-core, RPC routing between processes, and Electron main-process navigation.

## Three-Layer Architecture

The implementation consists of three cooperating layers:

| Layer | Responsibility | Key Source Location |
|-------|----------------|---------------------|
| **URL Parsing** | Converts `craftagents://` URLs into structured navigation objects | [`packages/server-core/src/handlers/rpc/system.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/server-core/src/handlers/rpc/system.ts) (lines 69-133) |
| **RPC Routing** | Receives `OPEN_URL` requests and pushes `deeplink.NAVIGATE` commands to renderers | [`packages/server-core/src/handlers/rpc/system.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/server-core/src/handlers/rpc/system.ts) (lines 80-110) |
| **Electron Navigation** | Forwards navigation RPCs to the appropriate window via the `sink` function | [`apps/electron/src/main/deep-link.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/apps/electron/src/main/deep-link.ts) (lines 320-340) |

## URL Parsing in Server-Core

The `parseInternalCraftAgentsDeepLink` function in [`packages/server-core/src/handlers/rpc/system.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/server-core/src/handlers/rpc/system.ts) validates and decomposes incoming URLs into structured navigation instructions.

```typescript
function parseInternalCraftAgentsDeepLink(parsed: URL): ParsedInternalDeepLink | null {
  if (parsed.protocol !== 'craftagents:') return null

  const host = parsed.hostname
  const pathParts = parsed.pathname.split('/').filter(Boolean)
  const windowMode = parsed.searchParams.get('window')

  // "window=focused/full" → let the client open a new window
  if (windowMode === 'focused' || windowMode === 'full') {
    return { requiresExternalOpen: true }
  }

  // OAuth callback – no navigation needed
  if (host === 'auth-callback') {
    return { handledNoop: true }
  }

  // Compound routes (e.g. allSessions, flagged, settings)
  if (COMPOUND_ROUTE_PREFIXES.has(host)) {
    const viewRoute = pathParts.length > 0 ? `${host}/${pathParts.join('/')}` : host
    return { navigation: { view: viewRoute } }
  }

  // Action routes (e.g. craftagents://action/close?reason=idle)
  if (host === 'action') {
    const action = pathParts[0]
    const actionParams = collectDeepLinkParams(parsed, pathParts[1])
    return { navigation: { action, actionParams } }
  }

  // Workspace-scoped routes
  if (host === 'workspace') {
    const workspaceId = pathParts[0]
    const routeType = pathParts[1]

    if (COMPOUND_ROUTE_PREFIXES.has(routeType)) {
      return { workspaceId, navigation: { view: pathParts.slice(1).join('/') } }
    }
    if (routeType === 'action') {
      const action = pathParts[2]
      return {
        workspaceId,
        navigation: { action, actionParams: collectDeepLinkParams(parsed, pathParts[3]) },
      }
    }
  }

  return null
}

```

The parser returns a `ParsedInternalDeepLink` object that specifies **what** to navigate to (view or action), **where** (workspace ID), or whether the link requires **external window handling**.

## RPC Routing and Navigation Dispatch

When the server receives an `OPEN_URL` request via `RPC_CHANNELS.shell.OPEN_URL`, the handler in [`packages/server-core/src/handlers/rpc/system.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/server-core/src/handlers/rpc/system.ts) routes the request based on the parsed result.

```typescript
server.handle(RPC_CHANNELS.shell.OPEN_URL, async (ctx, url: string) => {
  const parsed = new URL(url)

  // Internal deep-link handling
  if (parsed.protocol === 'craftagents:') {
    const deepLink = parseInternalCraftAgentsDeepLink(parsed)

    if (deepLink?.handledNoop) return               // auth-callback – ignore
    if (deepLink?.navigation?.view || deepLink?.navigation?.action) {
      // Target may be a different workspace → route accordingly
      const target = deepLink.workspaceId && deepLink.workspaceId !== ctx.workspaceId
        ? { to: 'workspace' as const, workspaceId: deepLink.workspaceId }
        : { to: 'client' as const, clientId: ctx.clientId }

      server.push(RPC_CHANNELS.deeplink.NAVIGATE, target, deepLink.navigation)
      return
    }

    // Window-management request or unknown shape → fall back to client
    const deepLinkResult = await requestClientOpenExternal(server, ctx.clientId, url)
    if (!deepLinkResult.opened) throw new Error(`Cannot open URL on client: ${deepLinkResult.error}`)
    return
  }

  // Normal external URLs (http, https, mailto, craftdocs)
  if (!['http:', 'https:', 'mailto:', 'craftdocs:'].includes(parsed.protocol)) {
    throw new Error('Only http, https, mailto, craftdocs, craftagents URLs are allowed')
  }
  const result = await requestClientOpenExternal(server, ctx.clientId, url)
  if (!result.opened) throw new Error(`Cannot open URL on client: ${result.error}`)
})

```

If the parsed deep-link contains a **navigation** payload (view or action), the server pushes `RPC_CHANNELS.deeplink.NAVIGATE` to the target renderer—either the current client or a specific workspace window. If the link requires a new window or uses an unrecognized shape, the server falls back to `requestClientOpenExternal`.

## Electron Main-Process Navigation

The `handleDeepLinkUrl` function in [`apps/electron/src/main/deep-link.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/apps/electron/src/main/deep-link.ts) (lines 320-340) receives the `deeplink.NAVIGATE` RPC and forwards it to the appropriate window via the **sink** function.

```typescript
if (target.view || target.action) {
  const navigation: DeepLinkNavigation = {
    view: target.view,
    action: target.action,
    actionParams: target.actionParams,
  }
  const wsId = target.workspaceId ?? windowManager.getWorkspaceForWindow(window.webContents.id)
  const resolvedClientId = resolveClientId?.(window.webContents.id)

  const clientId = resolvedClientId ?? (!resolveClientId ? preferredClientId : undefined)

  if (sink && clientId) {
    sink(RPC_CHANNELS.deeplink.NAVIGATE, { to: 'client', clientId }, navigation)
  } else if (sink && wsId) {
    sink(RPC_CHANNELS.deeplink.NAVIGATE, { to: 'workspace', workspaceId: wsId }, navigation)
  }
}

```

The **sink** function wraps `webContents.send`, delivering the navigation payload to the renderer process. The function determines the target window by checking the workspace ID through `windowManager.getWorkspaceForWindow` or resolving the client ID.

## Protocol Definitions

The deep-link navigation contract is defined in [`packages/shared/src/protocol/routing.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/shared/src/protocol/routing.ts):

```typescript
export interface DeepLinkNavigation {
  view?: string
  action?: string
  actionParams?: Record<string, string>
}

```

The `RPC_CHANNELS` constants are defined in [`packages/shared/src/protocol/events.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/shared/src/protocol/events.ts), exposing `shell.OPEN_URL` for incoming links and `deeplink.NAVIGATE` for renderer-side routing.

## Practical Implementation Examples

### Opening a Deep-Link from the Renderer

To open a specific session view from a React component:

```typescript
import { RPC_CHANNELS } from '@craft-agent/shared/protocol'

function openSession(sessionId: string) {
  const url = `craftagents://allSessions/session/${sessionId}`
  // Send to server – it will route internally via deeplink.NAVIGATE
  window.api.call(RPC_CHANNELS.shell.OPEN_URL, url)
}

```

The server parses the URL, identifies `allSessions/session/<id>`, builds `{ navigation: { view: 'allSessions/session/<id>' } }`, and pushes `deeplink.NAVIGATE` to the appropriate window, which renders the requested view.

### Requesting a New Focused Window

To open a workspace action in a new focused window:

```typescript
const url = 'craftagents://workspace/abc123/action/close?window=focused'
window.api.call(RPC_CHANNELS.shell.OPEN_URL, url)

```

The parser returns `{ requiresExternalOpen: true }` due to the `window=focused` parameter. The server falls back to `requestClientOpenExternal`, causing the Electron client to spawn a new focused window that loads workspace `abc123` and executes the `close` action.

### Handling Authentication Callbacks

To process an OAuth callback without triggering navigation:

```typescript
// The auth provider redirects to:
const callbackUrl = 'craftagents://auth-callback?code=xyz'

// In the renderer:
window.api.call(RPC_CHANNELS.shell.OPEN_URL, callbackUrl)

```

The parser returns `{ handledNoop: true }` for the `auth-callback` host. The server returns immediately without pushing navigation events, allowing the authentication subsystem to extract query parameters like `code` or `state` without disrupting the current UI state.

## Summary

- **Craft Agents OSS** implements deep linking via the `craftagents://` URL scheme, enabling external applications to launch specific workspaces, views, or actions within the Electron application.
- The **three-layer architecture** separates URL parsing in [`packages/server-core/src/handlers/rpc/system.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/server-core/src/handlers/rpc/system.ts), RPC routing through `shell.OPEN_URL` and `deeplink.NAVIGATE` channels, and Electron main-process navigation in [`apps/electron/src/main/deep-link.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/apps/electron/src/main/deep-link.ts).
- The parser supports **compound routes** (e.g., `allSessions`, `settings`), **action routes** (e.g., `action/close`), **workspace-scoped routes**, and special handling for **authentication callbacks** and **window management**.
- Navigation payloads follow the `DeepLinkNavigation` interface defined in [`packages/shared/src/protocol/routing.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/shared/src/protocol/routing.ts), containing optional `view`, `action`, and `actionParams` fields.

## Frequently Asked Questions

### What URL schemes does Craft Agents support?

Craft Agents supports `craftagents://` for internal deep linking, `craftdocs://` for document references, and standard external protocols including `http:`, `https:`, and `mailto:`. The `shell.OPEN_URL` handler in [`packages/server-core/src/handlers/rpc/system.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/server-core/src/handlers/rpc/system.ts) validates protocols and rejects unrecognized schemes with an explicit error message.

### How does Craft Agents handle OAuth callbacks?

OAuth callbacks use the `craftagents://auth-callback` host pattern. When `parseInternalCraftAgentsDeepLink` encounters this host, it returns `{ handledNoop: true }`, causing the RPC handler to return immediately without pushing navigation events. This allows the authentication flow to consume query parameters like `code` or `state` without triggering any UI navigation or view changes.

### Can deep links open new windows?

Yes. Appending `?window=focused` or `?window=full` to a `craftagents://` URL causes `parseInternalCraftAgentsDeepLink` to return `{ requiresExternalOpen: true }`. The server then invokes `requestClientOpenExternal`, prompting the Electron client to spawn a new BrowserWindow with the specified workspace or action loaded, either in focused or full-screen mode.

### What is the difference between view routes and action routes?

**View routes** (e.g., `craftagents://allSessions/session/123`) map to application screens and populate the `view` field in `DeepLinkNavigation`. **Action routes** (e.g., `craftagents://action/close?reason=idle`) trigger specific behaviors and populate both the `action` and `actionParams` fields. The parser in [`packages/server-core/src/handlers/rpc/system.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/server-core/src/handlers/rpc/system.ts) distinguishes these via the URL host—`action` for actions and compound route prefixes for views.