Can opencode web UI open projects via URL parameters?

HEAD
3f81402chore: generate
This post might have stale content, as HEAD is 185 commits ahead.

Is it possible to open a project in the opencode web UI by passing the path in the URL params or headers?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Now I have enough information to provide a comprehensive answer. Let me write a summary based on what I found in the source code.

##Yes, you can open a project in the opencode web UI by passing the path in URL parameters!

Based on the source code analysis, here's how it works:

###URL Structure

The opencode web UI uses base64-encoded project paths in the URL to open projects. The routing structure is:

/                           # Home page
/:dir                       # Open project at base64-encoded path
/:dir/session              # Open a new session for that project
/:dir/session/:id          # Open an existing session
/new-session?draftId=...   # Draft sessions

###How to Open a Project

The project path must be base64-encoded and placed as the first URL segment. For example:

# If your project is at `/home/user/myproject`
# Base64 encode it: L2hvbWUvdXNlci9teXByb2plY3Q=
# Then navigate to:
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=

# To open a new session:
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session

# To open an existing session:
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session/SESSION_ID

###Implementation Details

The implementation is in packages/app/src:

  1. URL Decoding (src/utils/base64.ts):

    • The decode64() function decodes the base64-encoded path from the URL
    • If decoding fails, it shows an error toast and redirects to home
  2. Directory Layout (src/pages/directory-layout.tsx):

    • Validates and decodes the directory parameter from the URL
    • Provides the directory context to child components
  3. Routing (src/app.tsx):

    • Route pattern: /:dir maps to DirectoryLayout component
    • The :dir parameter contains the base64-encoded path
  4. Navigation (throughout the codebase):

    • When navigating to a project, the code uses base64Encode() from @opencode-ai/core/util/encode
    • Example from src/pages/home.tsx: navigate(\/${base64Encode(directory)}`)`

###Query Parameters

The web UI also supports additional query parameters:

  • ?draftId=... - For draft sessions
  • ?prompt=... - Initial prompt for new sessions
  • ?auth_token=... - Authentication token (automatically stripped from URL after use)

###Important Notes

  • No HTTP headers are used for path passing - only URL parameters
  • The path must be properly base64-encoded
  • Invalid or non-existent paths will show an error and redirect to home
  • The directory encoding happens client-side, so you need to base64-encode the path before putting it in the URL
Avatar of bmpenuelas
Borja Penuelas
commented

Please provide the full list of URL params and/or URL segments that are supported.

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Perfect! Now I have all the information I need. Let me compile a comprehensive answer.

##Complete List of Supported URL Parameters and Segments

Based on my analysis of the opencode web UI source code, here's the comprehensive list of all supported URL parameters and path segments:

###URL Path Segments

SegmentTypeDescriptionExample
/RouteHome pagehttps://opencode.ai/
/:dirRoute ParameterBase64-encoded project directory pathhttps://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=
/sessionRoute SegmentSession sub-routehttps://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session
/session/:idRoute ParameterSession ID (optional)https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session/abc123
/new-sessionRouteDraft/new session pagehttps://opencode.ai/new-session

###Query Parameters (URL Search/Params)

ParameterScopeTypeDescriptionExample
draftId/new-session + session routesstringDraft session identifier?draftId=draft-123
prompt/new-session + /sessionstringInitial prompt text to populate in the composer?prompt=write%20hello%20world
auth_tokenGlobal (entry point)string (base64)Authentication token in base64 format (basic auth)?auth_token=b3BlbmNvZGU6c2VjcmV0

###Hash Fragments

FragmentScopeDescriptionExample
#message-{id}Session pagesScroll to and highlight a specific message by ID#message-msg-abc123

###File URL Query Parameters (For File Attachments)

When files are referenced in prompts, they can include selection parameters:

ParameterTypeDescriptionExample
startnumberStarting line number for file selection?start=10
endnumberEnding line number for file selection?end=20

###Complete URL Examples

# Home page
https://opencode.ai/

# Open project directory
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=

# Open project with new session
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session

# Open existing session
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session/session-id-123

# Open existing session and scroll to message
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session/session-id-123#message-msg-abc

# Draft session with initial prompt
https://opencode.ai/new-session?draftId=draft-123&prompt=write%20hello%20world

# Open session with initial prompt
https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session?prompt=explain%20this%20code

# With authentication (processed at entry, then removed from URL)
https://opencode.ai/?auth_token=b3BlbmNvZGU6c2VjcmV0

###Important Notes

  1. :dir must be base64-encoded: The project path is encoded using base64 (see @opencode-ai/core/util/encode)
  2. auth_token is auto-removed: After being processed at entry, the auth_token is automatically cleared from the URL for security
  3. Query parameters are preserved: When navigating between routes, existing query parameters and hash fragments are preserved in the URL
  4. File selection parameters: The start and end parameters are only used internally when files with line selections are referenced in prompts
  5. No header-based parameters: All configuration is passed through URL parameters only - HTTP headers are not used

###Source Code References

  • Routes: packages/app/src/app.tsx (lines 452-459)
  • Query params: packages/app/src/app.tsx, pages/session.tsx, pages/new-session.tsx
  • Auth token handling: packages/app/src/entry.tsx (lines 116, 157)
  • Hash scrolling: packages/app/src/pages/session/use-session-hash-scroll.ts
  • File selection params: packages/app/src/utils/prompt.ts (lines 23-35)
Avatar of bmpenuelas
Borja Penuelas
commented

Will navigating to a directory by adding its base64-encoded path in the URL, and starting an new session by adding /session , open the project? (i.e will the project corresponding to that directory be visible in the projects side bar?)

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Based on my thorough investigation, I now have a clear answer. Let me provide a comprehensive response.

##Answer

No, navigating directly to a URL with a base64-encoded directory path will NOT automatically add that project to the sidebar and show it in the projects list.

Here's what actually happens:

###When You Navigate via URL to /:dir/session

  1. The directory IS loaded: The session interface loads and works for that directory
  2. The project is NOT added to the sidebar: The project does NOT appear in the projects list on the left sidebar
  3. You can still work: You can still interact with sessions and files for that directory, but it's temporary
  4. On refresh/reload: The project will disappear from view because it was never added to the persistent projects list

###How Projects Get Added to the Sidebar

Projects are only added to the sidebar via explicit user action:

  1. From the Home page: Click "Open Project" → select a directory → the project is added to the sidebar via projects.open(directory)
  2. From the file picker: Use the keyboard shortcut or menu to open a project directory
  3. Programmatically: Call layout.projects.open(directory) (only happens internally through UI actions or deep links)

###Source Code Evidence

The key code is in packages/app/src/pages/layout.tsx:

function openProject(directory: string, navigate = true) {
  layout.projects.open(directory)  // This explicitly adds to projects list
  if (navigate) return navigateToProject(directory)
}

And in packages/app/src/pages/home.tsx:

function openProject(server: ServerConnection.Any, directory: string) {
  const serverCtx = global.createServerCtx(server)
  serverCtx.projects.open(directory)  // Explicitly adds project
  serverCtx.projects.touch(directory)
  navigate(`/${base64Encode(directory)}`)  // Then navigates
}

Notice that openProject is called from:

  • Home page (user clicking on recent projects or opening new one)
  • Deep links (custom protocol handlers)
  • Deep link handler in handleDeepLinks()

It is NOT called when you navigate to the URL directly.

###The currentProject() Lookup

There is a currentProject() function that tries to find the project in the sidebar, but it only finds existing projects - it doesn't add new ones:

const currentProject = createMemo(() => {
  const directory = currentDir()
  if (!directory) return
  const key = pathKey(directory)
  
  const projects = layout.projects.list()  // Look in existing list
  
  const sandbox = projects.find((p) => p.sandboxes?.some((item) => pathKey(item) === key))
  if (sandbox) return sandbox
  
  const direct = projects.find((p) => pathKey(p.worktree) === key)
  if (direct) return direct
  
  // ... returns undefined if not found
})

###Practical Implication

If you want to open a project via URL and have it appear in the sidebar, you need to:

  1. First open the project normally (via home page, file picker, etc.) - this adds it to the sidebar
  2. Then navigate to it via URL

Or, programmatically, the app needs to call projects.open(directory) before navigating to that URL.

Avatar of bmpenuelas
Borja Penuelas
commented

Is there any way in which the URL or message passing if embedded in an iframe could trigger adding a project to the sidebar?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Perfect! Now I have all the information I need. Let me write a comprehensive answer about iframe message passing and deep links.

##Yes! There are two mechanisms to trigger opening a project from an iframe:

This is the primary mechanism designed for iframe communication. You can add projects to the sidebar through the window.__OPENCODE__ object:

####Opening a Project

// From an iframe, set up the deep links before the app mounts
window.__OPENCODE__ = window.__OPENCODE__ || {}
window.__OPENCODE__.deepLinks = window.__OPENCODE__.deepLinks || []
window.__OPENCODE__.deepLinks.push('opencode://open-project?directory=/path/to/project')

// Then dispatch the event to trigger handling
window.dispatchEvent(new CustomEvent('opencode:deep-link', {
  detail: {
    urls: ['opencode://open-project?directory=/path/to/project']
  }
}))

####Opening a Project and Starting a Session

window.__OPENCODE__ = window.__OPENCODE__ || {}
window.__OPENCODE__.deepLinks = window.__OPENCODE__.deepLinks || []
window.__OPENCODE__.deepLinks.push('opencode://new-session?directory=/path/to/project&prompt=hello%20world')

window.dispatchEvent(new CustomEvent('opencode:deep-link', {
  detail: {
    urls: ['opencode://new-session?directory=/path/to/project&prompt=hello%20world']
  }
}))
FormatPurposeParametersExample
opencode://open-project?directory=...Add project to sidebardirectory (required)opencode://open-project?directory=/home/user/myproject
opencode://new-session?directory=...Add project & open new sessiondirectory (required), prompt (optional)opencode://new-session?directory=/home/user/myproject&prompt=write%20hello%20world

###How It Works

  1. Add deep links to window.__OPENCODE__.deepLinks: The app checks this array at startup and when events are dispatched
  2. Dispatch the opencode:deep-link event: The app listens for this CustomEvent and processes all URLs in the event detail
  3. App automatically:
    • Calls openProject(directory) which adds the project to the sidebar
    • Creates a new session if using new-session deep link
    • Pre-populates the prompt if provided
    • Navigates to the session

###Implementation Details

From the source code (packages/app/src/pages/layout.tsx):

onMount(() => {
  const handler = (event: Event) => {
    const detail = (event as CustomEvent<{ urls: string[] }>).detail
    const urls = detail?.urls ?? []
    if (urls.length === 0) return
    handleDeepLinks(urls)
  }

  handleDeepLinks(drainPendingDeepLinks(window))  // Process any pre-loaded links
  makeEventListener(window, deepLinkEvent, handler as EventListener)  // Listen for events
})

const handleDeepLinks = (urls: string[]) => {
  if (!server.isLocal()) return  // Only works on local servers

  for (const directory of collectOpenProjectDeepLinks(urls)) {
    void openProject(directory)  // Adds to sidebar!
  }

  for (const link of collectNewSessionDeepLinks(urls)) {
    void openProject(link.directory, false)  // Adds to sidebar!
    // ... navigates to session
  }
}

###Important Constraints

⚠️ Deep links only work on local servers (!server.isLocal() check). The code explicitly returns early if the server is not local, so this feature is restricted to:

  • localhost
  • 127.0.0.1
  • Local development servers

###Complete iframe Example

<!DOCTYPE html>
<html>
<head>
  <title>OpenCode Iframe Integration</title>
</head>
<body>
  <iframe 
    id="opencode-iframe"
    src="http://localhost:4096/"
    style="width: 100%; height: 100vh; border: none;"
  ></iframe>

  <script>
    // Access the iframe's window
    const iframeEl = document.getElementById('opencode-iframe')
    const iframeWindow = iframeEl.contentWindow

    // Function to open a project
    function openProjectInOpenCode(projectPath, prompt = null) {
      // Ensure the __OPENCODE__ object exists
      iframeWindow.__OPENCODE__ = iframeWindow.__OPENCODE__ || {}
      iframeWindow.__OPENCODE__.deepLinks = iframeWindow.__OPENCODE__.deepLinks || []

      // Create the deep link URL
      let url = `opencode://open-project?directory=${encodeURIComponent(projectPath)}`
      
      if (prompt) {
        url = `opencode://new-session?directory=${encodeURIComponent(projectPath)}&prompt=${encodeURIComponent(prompt)}`
      }

      // Add to the queue
      iframeWindow.__OPENCODE__.deepLinks.push(url)

      // Dispatch the event
      iframeWindow.dispatchEvent(new CustomEvent('opencode:deep-link', {
        detail: { urls: [url] }
      }))
    }

    // Wait for iframe to load, then open a project
    iframeEl.onload = () => {
      setTimeout(() => {
        openProjectInOpenCode('/home/user/my-project', 'write hello world')
      }, 1000)
    }
  </script>
</body>
</html>

###Source Code References

  • Deep link parsing: packages/app/src/pages/layout/deep-links.ts
  • Deep link handling: packages/app/src/pages/layout.tsx lines 1365-1405
  • Window type definition: packages/app/src/app.tsx (lines with __OPENCODE__)
  • Test examples: packages/app/src/pages/layout/helpers.test.ts

###Key Difference from Direct URL Navigation

MethodAdds to Sidebar?Works Without Opening First?
Direct URL (/:dir/session)❌ No❌ No, project must be opened first
Deep Links✅ Yes✅ Yes, adds project automatically

The deep link mechanism is specifically designed to solve the problem you asked about - it automatically adds the project to the sidebar when triggered from an iframe or programmatically!

Avatar of bmpenuelas
Borja Penuelas
commented

So it wouldn't work for a web server started with "opencode web --hostname 0.0.0.0"? Is there any solution to open a project for a web server that is accessible on LAN?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Based on my investigation, I can now provide a comprehensive answer about the limitation and potential solutions.

###Why It's Restricted

The code explicitly checks if the server is "local" before processing deep links:

// packages/app/src/pages/layout.tsx
const handleDeepLinks = (urls: string[]) => {
  if (!server.isLocal()) return  // ← This check blocks non-local servers
  
  for (const directory of collectOpenProjectDeepLinks(urls)) {
    void openProject(directory)
  }
  // ...
}

The isLocal() check is defined in packages/app/src/context/server.tsx:

function isLocalHost(url: string) {
  const host = url.replace(/^https?:\/\//, "").split(":")[0]
  if (host === "localhost" || host === "127.0.0.1") return "local"
  // Returns undefined for any other hostname (including LAN IPs like 192.168.x.x)
}

So only these exact hostnames pass the check:

  • localhost
  • 127.0.0.1

Any other hostname fails, including:

  • LAN IP addresses (e.g., 192.168.1.100)
  • Hostnames (e.g., mycomputer.local)
  • IPv6 loopback (::1 is NOT checked, even though it's loopback)

##Solutions

###Option 1: Use a Reverse Proxy or SSH Tunnel (Workaround)

Access the server through localhost even though it's running on 0.0.0.0:

# On the machine running opencode web
ssh -L 4096:localhost:4096 user@remote-machine

# Then access via: http://localhost:4096

Or use a reverse proxy that maps your LAN hostname to localhost:

# nginx config
server {
    listen 80;
    server_name mycomputer.local;
    location / {
        proxy_pass http://127.0.0.1:4096;
    }
}

###Option 2: Fix the Code (Extend the isLocalHost Check)

You can modify the isLocalHost function to accept more hostnames. This is a code change you could contribute:

// packages/app/src/context/server.tsx
function isLocalHost(url: string) {
  const host = url.replace(/^https?:\/\//, "").split(":")[0]
  if (host === "localhost" || host === "127.0.0.1") return "local"
  
  // Add LAN/IPv6 support
  if (host === "::1") return "local"  // IPv6 loopback
  if (host.startsWith("192.168.")) return "local"  // Common private range
  if (host.startsWith("10.")) return "local"  // Private range
  if (host.startsWith("172.16.") || host.startsWith("172.31.")) return "local"  // Private range
  if (host.endsWith(".local")) return "local"  // mDNS hostnames
}

###Option 3: Use the Desktop App Instead

The desktop app doesn't have this restriction because it uses a different mechanism. The deep links work through the __OPENCODE__ window object, and the desktop app runs locally.

###Option 4: Fall Back to Manual Opening

For non-local servers, users would need to:

  1. Navigate to the home page
  2. Click "Open Project"
  3. Select the directory from the file picker

This works because the file picker can browse the remote server's filesystem.


##Why This Restriction Exists

This is likely a security measure to prevent:

  • Remote code execution if an attacker crafts malicious deep links
  • Unauthorized project opening on shared servers
  • Path traversal attacks to access files outside permitted directories

Deep links directly execute openProject() which adds any directory to the sidebar without user interaction - so they're restricted to local, trusted contexts.


##Workarounds Summary

ApproachFeasibilityEffort
SSH tunnel to localhost✅ HighLow
Reverse proxy to localhost✅ HighMedium
Modify source code✅ HighMedium
Use desktop app✅ MediumDepends on setup
Manual opening via UI✅ Always worksHigh (tedious)

The SSH tunnel approach is simplest if you control both machines - it lets you access the server as localhost while your web server listens on 0.0.0.0.


END OF POST

Can opencode web UI open projects via URL parameters? — anomalyco/opencode