September 5, 2026 · 10 min read
How to rename, add and delete Sidekick workspaces after the shutdown

Sidekick was acquired by Perplexity and shut down in 2025. The stated successor is Comet Browser, but its idea of a workspace is different enough that plenty of people are still running Sidekick.
Most of Sidekick keeps working offline after the shutdown. Three things do not: renaming a workspace, adding one, and deleting one. All three are dead in the UI.
The reason is that workspace operations were built assuming they could sync to Sidekick's servers. Those servers are gone, so the UI refuses the change.
This post shows how to make the change anyway, by writing to the extension's own storage over the Chrome DevTools Protocol. Everything here runs on macOS and is what I actually used on my own machine.
Where the names really live
First, where does a workspace name get stored?
The Preferences file is only a display cache
Sidekick's settings file is here:
~/Library/Application Support/Sidekick/Default/Preferences
sidekick.tab_menu inside that file does contain workspace names, but it is a cache for drawing the menu. Editing it accomplishes nothing, because Sidekick overwrites it from internal data on the next launch.
The real data
The real data lives in the chrome.storage.local of Sidekick's internal extension, mcjlamohcooanphmebaiigheeeoplihb.
Four keys hold workspace-related data:
{userId}_workspaces → names, avatars, settings
{userId}_sessions → session names, one per workspace
{userId}_app-accounts-by-workspace → app accounts per workspace
{userId}_backend_cache_/desktop/workspaces → backend cache
userId is the UUID of your Sidekick account. The steps below find it for you.
Any workspace operation has to update all of these consistently. _backend_cache_ in particular: skip it and your change gets restored from the cache on the next launch.
Renaming a workspace
Requirements
pip3 install websocket-client
Step 1: Start Sidekick in debug mode
Quit the running instance first.
osascript -e 'quit app "Sidekick"'
Wait about three seconds, then start it with remote debugging on.
/Applications/Sidekick.app/Contents/MacOS/Sidekick \
--remote-debugging-port=9222 \
'--remote-allow-origins=*' &
Check that it came up:
curl -s http://localhost:9222/json/list | python3 -m json.tool | head -5
If you get JSON back, you are connected.
Step 2: List the workspaces you have
This prints the current names and their IDs.
import json, websocket, urllib.request
# Connect to the extension's background page
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = """
new Promise((resolve) => {
chrome.storage.local.get(null, (items) => {
var result = [];
Object.keys(items).forEach(k => {
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {
var workspaces = items[k];
if (Array.isArray(workspaces)) {
workspaces.forEach(w => {
result.push({id: w.id, name: w.name});
});
}
}
});
resolve(JSON.stringify(result));
});
})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
workspaces = json.loads(resp['result']['result']['value'])
for w in workspaces:
print(f" {w['name']} (ID: {w['id']})")
ws.close()
Output looks like this:
[email protected] (ID: e0f3eaa0-...)
Workspace A (ID: a88608c8-...)
Workspace B (ID: 7b4fb04a-...)
Step 3: Write the new name
Put in the workspace ID from step 2 and the name you want.
import json, websocket, urllib.request
# === edit these ===
TARGET_WORKSPACE_ID = "a88608c8-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # from step 2
NEW_NAME = "New name"
# ==================
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
var targetId = "{TARGET_WORKSPACE_ID}";
var newName = "{NEW_NAME}";
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var changeCount = 0;
Object.keys(allItems).forEach(k => {{
// Name and avatar in _workspaces
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {{
var workspaces = allItems[k];
if (Array.isArray(workspaces)) {{
var oldName = null;
workspaces.forEach((w, i) => {{
if (w.id === targetId) {{
oldName = w.name;
workspaces[i].name = newName;
workspaces[i].avatar.text = newName.substring(0, 1);
changeCount++;
}}
}});
if (oldName) {{
changes[k] = workspaces;
// The matching session name
var sessionsKey = k.replace('_workspaces', '_sessions');
if (allItems[sessionsKey]) {{
var sessions = allItems[sessionsKey];
Object.keys(sessions).forEach(sk => {{
if (sessions[sk].name === oldName) {{
sessions[sk].name = newName;
changeCount++;
}}
}});
changes[sessionsKey] = sessions;
}}
// And the backend cache
Object.keys(allItems).forEach(ck => {{
if (ck.includes('backend_cache') && ck.includes('workspaces')) {{
var val = allItems[ck];
if (typeof val === 'string' && val.includes(oldName)) {{
changes[ck] = val.split(oldName).join(newName);
changeCount++;
}}
}}
}});
}}
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{success: true, changeCount: changeCount}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
print(f"Done: updated {result['changeCount']} places")
ws.close()
Step 4: Restart Sidekick
# Quit the debug-mode instance
osascript -e 'quit app "Sidekick"'
# Start it normally
open -a Sidekick
Open the workspace list and the new name is there.
Adding a workspace
Adding means writing to three places at once: _workspaces, _app-accounts-by-workspace and _backend_cache_.
import json, websocket, urllib.request, uuid
# === edit these ===
NEW_NAME = "New workspace"
BG_COLOR = "#4CAF50" # avatar background
FG_COLOR = "#ffffff" # avatar text color
# ==================
new_ws_id = str(uuid.uuid4())
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var newWsId = "{new_ws_id}";
var newName = "{NEW_NAME}";
Object.keys(allItems).forEach(k => {{
// 1. Append to _workspaces
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {{
var workspaces = allItems[k];
if (Array.isArray(workspaces)) {{
workspaces.push({{
avatar: {{
backgroundColor: "{BG_COLOR}",
foregroundColor: "{FG_COLOR}",
text: newName.substring(0, 1),
type: "Colored"
}},
id: newWsId,
isMinimized: false,
isRemovable: true,
isTeamWorkspace: false,
name: newName,
orderAppIds: [],
partitionDomain: newWsId,
pinnedOrderAppIds: [],
sandbox: false,
teamUuid: "00000000-0000-0000-0000-000000000000"
}});
changes[k] = workspaces;
// 2. Empty entry in _app-accounts-by-workspace
var appKey = k.replace('_workspaces', '_app-accounts-by-workspace');
if (allItems[appKey]) {{
var appAccounts = allItems[appKey];
appAccounts[newWsId] = {{}};
changes[appKey] = appAccounts;
}}
}}
}}
// 3. Append to _backend_cache_
if (k.includes('backend_cache') && k.endsWith('workspaces')) {{
var cache = allItems[k];
if (typeof cache === 'string') {{
var parsed = JSON.parse(cache);
parsed.push({{
uuid: newWsId,
name: newName,
background: {{
type: "Color",
value: {{bg: "{BG_COLOR}", letterColor: "{FG_COLOR}"}}
}},
sandbox: false,
pinned_services_order: {{order: []}},
services_order: {{order: []}},
session_key: null,
last_active_service_id: null,
can_delete: true,
team_workspace: false,
team_uuid: "00000000-0000-0000-0000-000000000000"
}});
changes[k] = JSON.stringify(parsed);
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{success: true, newId: newWsId}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
print(f"Added: ID={result['newId']}")
ws.close()
Restart Sidekick and the new workspace is in the list.
Deleting a workspace
Deleting is the reverse: remove the entries from _workspaces, _app-accounts-by-workspace, _sessions and _backend_cache_.
Do not delete the default workspace, the one with isRemovable: false. Sidekick may fail to start.
import json, websocket, urllib.request
# === edit this ===
TARGET_WORKSPACE_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # from step 2
# =================
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
var targetId = "{TARGET_WORKSPACE_ID}";
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var deletedName = null;
Object.keys(allItems).forEach(k => {{
// 1. Remove from _workspaces
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {{
var workspaces = allItems[k];
if (Array.isArray(workspaces)) {{
var target = workspaces.find(w => w.id === targetId);
if (target) {{
if (!target.isRemovable) {{
resolve(JSON.stringify({{
success: false,
reason: "The default workspace cannot be deleted"
}}));
return;
}}
deletedName = target.name;
changes[k] = workspaces.filter(w => w.id !== targetId);
// 2. Remove from _app-accounts-by-workspace
var appKey = k.replace('_workspaces', '_app-accounts-by-workspace');
if (allItems[appKey]) {{
var appAccounts = allItems[appKey];
delete appAccounts[targetId];
changes[appKey] = appAccounts;
}}
// 3. Remove the matching session
var sessionsKey = k.replace('_workspaces', '_sessions');
if (allItems[sessionsKey] && deletedName) {{
var sessions = allItems[sessionsKey];
Object.keys(sessions).forEach(sk => {{
if (sessions[sk].name === deletedName) {{
delete sessions[sk];
}}
}});
changes[sessionsKey] = sessions;
}}
}}
}}
}}
// 4. Remove from _backend_cache_
if (k.includes('backend_cache') && k.endsWith('workspaces')) {{
var cache = allItems[k];
if (typeof cache === 'string') {{
var parsed = JSON.parse(cache);
var filtered = parsed.filter(w => w.uuid !== targetId);
if (filtered.length !== parsed.length) {{
changes[k] = JSON.stringify(filtered);
}}
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{
success: true,
deletedName: deletedName
}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
if result['success']:
print(f"Deleted: {result['deletedName']}")
else:
print(f"Failed: {result['reason']}")
ws.close()
Restart Sidekick and the workspace is gone.
Why this works at all
Sidekick still runs on locally cached data after the shutdown. Workspace names are read in this order:
chrome.storage.localin the internal extension, which is the real datasidekick.tab_menuin the Preferences file, a display cache generated while running- Sidekick's servers, which no longer answer
Changing a name in the UI fails because it wants to write to the servers first. Writing to chrome.storage.local succeeds because that is where the data is read from on the next launch.
None of this is specific to Sidekick. Any Chromium-based browser will let you at its internal data the same way: start it with --remote-debugging-port, connect over WebSocket, and evaluate JavaScript.
Bonus: removing the "Sidekick Browser is Shutting Down" overlay
After the shutdown, every launch greets you with a "Sidekick Browser is Shutting Down" overlay. Dismissing it every single time gets old.
It comes from overlay.js in the internal extension, which renders a modal called product-shutdown-modal. One edit to that file removes it.
Quit Sidekick, then back the file up:
OVERLAY_JS=~/Library/Application\ Support/Sidekick/Default/Extensions/\
mcjlamohcooanphmebaiigheeeoplihb/*/overlay/overlay.js
cp $OVERLAY_JS "${OVERLAY_JS}.backup"
Then change one thing:
import os, glob
# Find overlay.js
pattern = os.path.expanduser(
'~/Library/Application Support/Sidekick/Default/Extensions/'
'mcjlamohcooanphmebaiigheeeoplihb/*/overlay/overlay.js'
)
overlay_path = glob.glob(pattern)[0]
with open(overlay_path, 'r') as f:
content = f.read()
# Point the shutdown modal at a component index that does not exist
old = '"product-shutdown-modal"?33'
new = '"product-shutdown-modal"?-1'
if old in content:
content = content.replace(old, new)
with open(overlay_path, 'w') as f:
f.write(content)
print("Patched")
else:
print("Nothing to patch. The index differs between versions.")
overlay.js is a Svelte router for modals. It maps a modal ID to a component index, and product-shutdown-modal returns index 33. Setting it to -1 means no matching component, so nothing renders.
If Sidekick ever auto-updates, the extension's version directory changes and the patch is gone. Given that the service is dead, an update is unlikely. To undo it:
OVERLAY_JS=~/Library/Application\ Support/Sidekick/Default/Extensions/\
mcjlamohcooanphmebaiigheeeoplihb/*/overlay/overlay.js
cp "${OVERLAY_JS}.backup" "$OVERLAY_JS"
Bonus: setting icons on custom web apps
Sidekick lets you add any website to the left sidebar as an app, but since the shutdown you cannot set or change its icon. The API that uploaded icon images to Sidekick's CDN, i.meetsidekick.com, is gone.
You can write the image URL straight into customIconUrl inside _applications instead.
Step 1: List your custom apps
With Sidekick running in debug mode:
import json, websocket, urllib.request
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = """
new Promise((resolve) => {
chrome.storage.local.get(null, (items) => {
var result = [];
Object.keys(items).forEach(k => {
if (k.endsWith('_applications')) {
var apps = items[k];
Object.keys(apps).forEach(id => {
var app = apps[id];
if (app.customUrl) {
result.push({
id: id,
name: app.name,
url: app.customUrl,
hasIcon: !!(app.customIconUrl || app.iconUrl),
iconUrl: app.customIconUrl || app.iconUrl || null
});
}
});
}
});
resolve(JSON.stringify(result));
});
})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
apps = json.loads(resp['result']['result']['value'])
for app in apps:
icon = "OK" if app['hasIcon'] else "none"
print(f" [{icon}] {app['name']} (ID: {app['id']})")
print(f" URL: {app['url'][:60]}")
ws.close()
Step 2: Set the icon
Any of these work as the image: an external URL, a favicon service, or a base64 data URI.
import json, websocket, urllib.request
# === edit these ===
TARGET_APP_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # from step 1
ICON_URL = "https://www.google.com/s2/favicons?domain=example.com&sz=128"
# ==================
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var appName = null;
Object.keys(allItems).forEach(k => {{
if (k.endsWith('_applications')) {{
var apps = allItems[k];
if (apps["{TARGET_APP_ID}"]) {{
appName = apps["{TARGET_APP_ID}"].name;
apps["{TARGET_APP_ID}"].customIconUrl = "{ICON_URL}";
changes[k] = apps;
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{success: true, appName: appName}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
print(f"Icon set on: {result['appName']}")
ws.close()
Restart Sidekick and the icon shows up in the left sidebar.
If hunting down an icon URL is a bother, Google's favicon service does the job:
https://www.google.com/s2/favicons?domain=example.com&sz=128
Summary
The shape is the same for all three operations:
- Start Sidekick with
--remote-debugging-port=9222 - Reach
chrome.storage.localover the Chrome DevTools Protocol - Update
_workspaces,_sessions,_app-accounts-by-workspaceand_backend_cache_consistently - Restart Sidekick
What each operation touches:
| Operation | _workspaces |
_sessions |
_app-accounts |
_backend_cache |
|---|---|---|---|---|
| Rename | name and avatar | name | not touched | name |
| Add | add entry | not touched | add empty entry | add entry |
| Delete | remove entry | remove entry | remove entry | remove entry |
The shutdown overlay is one patched line in the internal extension's overlay.js, and custom app icons come down to writing customIconUrl in _applications.
Sidekick shutting down is also the reason I ended up building my own browser. That story is in the first post of this series.