Can opencode web UI open projects via URL parameters?
Is it possible to open a project in the opencode web UI by passing the path in the URL params or headers?
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:
-
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
- The
-
Directory Layout (
src/pages/directory-layout.tsx):- Validates and decodes the directory parameter from the URL
- Provides the directory context to child components
-
Routing (
src/app.tsx):- Route pattern:
/:dirmaps toDirectoryLayoutcomponent - The
:dirparameter contains the base64-encoded path
- Route pattern:
-
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)}`)`
- When navigating to a project, the code uses
###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
Please provide the full list of URL params and/or URL segments that are supported.
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
| Segment | Type | Description | Example |
|---|---|---|---|
/ | Route | Home page | https://opencode.ai/ |
/:dir | Route Parameter | Base64-encoded project directory path | https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q= |
/session | Route Segment | Session sub-route | https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session |
/session/:id | Route Parameter | Session ID (optional) | https://opencode.ai/L2hvbWUvdXNlci9teXByb2plY3Q=/session/abc123 |
/new-session | Route | Draft/new session page | https://opencode.ai/new-session |
###Query Parameters (URL Search/Params)
| Parameter | Scope | Type | Description | Example |
|---|---|---|---|---|
draftId | /new-session + session routes | string | Draft session identifier | ?draftId=draft-123 |
prompt | /new-session + /session | string | Initial prompt text to populate in the composer | ?prompt=write%20hello%20world |
auth_token | Global (entry point) | string (base64) | Authentication token in base64 format (basic auth) | ?auth_token=b3BlbmNvZGU6c2VjcmV0 |
###Hash Fragments
| Fragment | Scope | Description | Example |
|---|---|---|---|
#message-{id} | Session pages | Scroll 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:
| Parameter | Type | Description | Example |
|---|---|---|---|
start | number | Starting line number for file selection | ?start=10 |
end | number | Ending 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
:dirmust be base64-encoded: The project path is encoded using base64 (see@opencode-ai/core/util/encode)auth_tokenis auto-removed: After being processed at entry, the auth_token is automatically cleared from the URL for security- Query parameters are preserved: When navigating between routes, existing query parameters and hash fragments are preserved in the URL
- File selection parameters: The
startandendparameters are only used internally when files with line selections are referenced in prompts - 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)
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?)
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
- The directory IS loaded: The session interface loads and works for that directory
- The project is NOT added to the sidebar: The project does NOT appear in the projects list on the left sidebar
- You can still work: You can still interact with sessions and files for that directory, but it's temporary
- 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:
- From the Home page: Click "Open Project" → select a directory → the project is added to the sidebar via
projects.open(directory) - From the file picker: Use the keyboard shortcut or menu to open a project directory
- 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:
- First open the project normally (via home page, file picker, etc.) - this adds it to the sidebar
- Then navigate to it via URL
Or, programmatically, the app needs to call projects.open(directory) before navigating to that URL.
Is there any way in which the URL or message passing if embedded in an iframe could trigger adding a project to the sidebar?
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:
###1. Deep Links via Window Object (Recommended)
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']
}
}))
###Deep Link URL Formats
| Format | Purpose | Parameters | Example |
|---|---|---|---|
opencode://open-project?directory=... | Add project to sidebar | directory (required) | opencode://open-project?directory=/home/user/myproject |
opencode://new-session?directory=... | Add project & open new session | directory (required), prompt (optional) | opencode://new-session?directory=/home/user/myproject&prompt=write%20hello%20world |
###How It Works
- Add deep links to
window.__OPENCODE__.deepLinks: The app checks this array at startup and when events are dispatched - Dispatch the
opencode:deep-linkevent: The app listens for this CustomEvent and processes all URLs in the event detail - App automatically:
- Calls
openProject(directory)which adds the project to the sidebar - Creates a new session if using
new-sessiondeep link - Pre-populates the prompt if provided
- Navigates to the session
- Calls
###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:
localhost127.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.tsxlines 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
| Method | Adds 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!
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?
Based on my investigation, I can now provide a comprehensive answer about the limitation and potential solutions.
##No, deep links will NOT work for a web server accessible on LAN with --hostname 0.0.0.0
###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:
localhost127.0.0.1
Any other hostname fails, including:
- LAN IP addresses (e.g.,
192.168.1.100) - Hostnames (e.g.,
mycomputer.local) - IPv6 loopback (
::1is 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:
- Navigate to the home page
- Click "Open Project"
- 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
| Approach | Feasibility | Effort |
|---|---|---|
SSH tunnel to localhost | ✅ High | Low |
Reverse proxy to localhost | ✅ High | Medium |
| Modify source code | ✅ High | Medium |
| Use desktop app | ✅ Medium | Depends on setup |
| Manual opening via UI | ✅ Always works | High (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