What an extension is
Octopus keeps your tasks, terminals, AI sessions, docs and git in one window. An extension adds what's missing for your work: your logs, an internal API, a dashboard, a standup summary. It gets a button of its own in the activity bar and a full section, beside the projects.
- Plain files, no build step. A folder with a
manifest.json, anindex.htmland whatever scripts and styles it needs. - Isolated. It runs in a frame of its own and talks to Octopus only through its SDK,
window.octopus. - Allowed by you. It reaches only what its manifest asks for, and only after you say yes.
- Live. Octopus reloads it every time one of its files changes.
Create one with Claude
The quickest way is to describe it. Octopus writes the extension with Claude Code next to a live preview, and you refine it by asking.
- Open the Extensions section and press Create with Claude.
- Give it a Name and say What it should do, as precisely as you can: “Query our Loki logs: ask in plain words, see the volume over time, group errors into patterns, open a trace.” If you have more than one Claude account, pick the one that writes it.
- A Claude Code session starts in the extension's folder, beside the page. Watch it take shape, and ask for changes in the session. From its header the Claude pane folds to a strip at the side or expands over the page, and stays as you left it.
- When it asks for permissions (a site, your tasks, Claude), Octopus shows you what and waits for your yes.
- Press Done. To change it later, press Edit on its page: a new session picks up where you left off.
Every new extension comes with a CLAUDE.md that teaches Claude the rules and the whole SDK, so what it writes follows this guide. You can open the folder in your editor at any time and change it by hand.
The files
Every extension is a folder in ~/.octopus/extensions/. The folder's name is the extension's id.
~/.octopus/extensions/standup/
├── manifest.json # name, icon, version, permissions
├── index.html # the page Octopus shows
├── app.js # your code, an ES module
├── CLAUDE.md # the rules and the SDK, for Claude (made for you)
└── README.md # optional: shown in the marketplace
Octopus watches the folder: a new extension shows up as soon as its manifest.json is there, and a change to any file reloads it.
Tutorial: a standup note
Let's build one by hand. Standup lists what's in progress and in review across every project, opens a task on a click, and asks Claude for a three-line note you can paste in your team's chat. It needs two permissions: tasks and claude.
1. The manifest
Create the folder ~/.octopus/extensions/standup and, in it:
{
"name": "Standup",
"description": "What's in progress and in review, and a standup note written by Claude.",
"icon": "comment-discussion",
"version": "0.1.0",
"author": "Your name",
"stage": "alpha",
"sdk": 1,
"permissions": { "tasks": true, "claude": true }
}
The Standup button appears in the activity bar right away. The first time you open it, Octopus asks whether it can read your tasks and use Claude.
2. The page
Link Octopus's stylesheet and SDK, both served by Octopus itself, then your module.
index.html<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Standup</title>
<link rel="stylesheet" href="/__octopus/base.css" />
<script src="/__octopus/sdk.js"></script>
<style>
body { padding: 16px 20px; }
ul { list-style: none; padding: 0; display: grid; gap: 4px; }
li { padding: 6px 8px; border-radius: var(--radius-sm); cursor: pointer; }
li:hover { background: var(--bg-hover); }
#note { white-space: pre-wrap; }
</style>
</head>
<body>
<div class="toolbar">
<button class="primary" id="write">Write the note</button>
<button class="ghost" id="copy">Copy</button>
</div>
<ul id="tasks"></ul>
<div class="card" id="note"><span class="muted">No note yet.</span></div>
<script type="module" src="app.js"></script>
</body>
</html>
3. The code
Wait for octopus.ready() before anything else: it says which extension this is, the theme and the projects.
const { projects } = await octopus.ready();
const projectName = Object.fromEntries(projects.map((p) => [p.id, p.name]));
const STATUS = { doing: "In progress", review: "Review" };
const list = document.getElementById("tasks");
const note = document.getElementById("note");
// The open work, across every project: a row per task, a click shows it on its board.
async function load() {
const tasks = (await octopus.tasks.list()).filter((t) => STATUS[t.status]);
list.replaceChildren(...tasks.map((t) => {
const li = document.createElement("li");
const badge = Object.assign(document.createElement("span"), { className: "badge", textContent: STATUS[t.status] });
const where = Object.assign(document.createElement("span"), { className: "faint", textContent: projectName[t.projectId] ?? "No project" });
li.append(badge, " ", t.title, " ", where);
li.onclick = () => octopus.tasks.reveal(t.id);
return li;
}));
return tasks;
}
const write = document.getElementById("write");
write.onclick = async () => {
write.disabled = true;
note.textContent = "Claude is writing…";
try {
const lines = (await load()).map((t) => `- ${STATUS[t.status]}: ${t.title} (${projectName[t.projectId] ?? "no project"})`);
note.textContent = await octopus.claude.ask(
`Write a standup note in three short lines from these tasks. Plain text, no preamble.\n${lines.join("\n")}`,
{ model: "haiku" },
);
} catch (err) {
note.textContent = err.message;
} finally {
write.disabled = false;
}
};
document.getElementById("copy").onclick = () => {
octopus.copy(note.textContent);
octopus.toast("Copied", "success");
};
// Fresh every time the section is shown.
octopus.on("visible", (shown) => shown && load());
load();
4. Try it
Save, and Octopus reloads the section. Press Write the note: a few seconds later Claude's note is there, ready to copy. Everything else is the same loop: change a file, look, change again. When it's done, drop "stage" from the manifest and share it.
Something doesn't work? Every SDK call returns a promise that rejects with an Error saying why: a permission the manifest doesn't declare, a host that isn't in permissions.http, no project open. Show err.message on the page while you build.
The manifest
manifest.json says what the extension is and what it may reach. Only name is required.
| Field | What it is |
|---|---|
name | Shown in the activity bar, the list and the marketplace. |
description | One or two sentences on what it does. Claude reads it too, when it writes or edits the extension. |
icon | A codicon name: pulse, graph, database, output, beaker… |
version | Like 1.2.0. Compared with the marketplace's to offer updates. |
author | You, or your team. |
stage | "alpha" or "beta" while it isn't finished: a label in the list and the marketplace. Leave it out once it is. |
entry | The page to show. Default index.html. |
sdk | The SDK protocol it was written for: 1. |
permissions | What it may reach besides its own files. See below. |
dependencies | The extensions it needs, with the oldest version that will do: { "calendar": "1.2.0" }. See Extensions that need others. |
Permissions
An extension starts with nothing but its own page. Each permission it declares is shown to you before it first runs, and Octopus asks again whenever the list grows. Every call is checked by Octopus's backend, not by the page. Ask only for what you need.
| Permission | Lets it |
|---|---|
http | Send requests through octopus.http.fetch, only to these URL prefixes: ["https://api.example.com/v1", "http://loki.internal"]. |
claude | Ask Claude Code with octopus.claude.ask and chat, open Claude sessions in a project, show them in its page and close the ones it opened. |
tasks | Read your tasks, create new ones, change them (status, priority, tags, blocked, archived), show one on its board. |
terminals | See Octopus's terminals and what they show, read-only: nothing can be typed into them. |
docs | Write Markdown and SVG files inside a project's documentation folder, and open them. |
statusBar | Show an item of its own in the Mac's menu bar. |
shared | Read what other extensions share, by their ids: ["calendar"]. |
webapp | Show a website itself. See A website as an extension. |
Look like Octopus
Link /__octopus/base.css and the extension follows Octopus's theme, light or dark, as it changes. Use its CSS variables for every colour, never hard-coded ones:
- Backgrounds:
--bg-editor,--bg-widget,--bg-input,--bg-hover,--bg-selected, and--border. - Text:
--fg,--fg-strong,--fg-muted,--fg-faint,--accent,--link. - States:
--error,--warn,--success; syntax colours--hl-keyword,--hl-string,--hl-number… - Shapes and type:
--radius-*,--font-ui,--font-mono.
Classes ready to use: button.primary, button.ghost, .card, .badge (with .error, .warn, .success), .toolbar, .muted, .faint, .mono, .spinner. Aim for a dense, quiet UI like an editor's: 13px text, small paddings, no big headers.
Drawing on a canvas or with a chart library? Redraw on octopus.on("theme", …): the variables already hold the new colours.
The SDK
/__octopus/sdk.js defines window.octopus. Every call returns a promise.
Start
const { extension, theme, projects, workProjectId } = await octopus.ready();
HTTP
Made by Octopus, not the page: no CORS, and the Mac's network (your VPN) applies. Any status resolves; only a failure to connect rejects. Up to 64 MB.
const res = await octopus.http.fetch("https://api.example.com/v1/deploys?env=prod", {
method: "POST", // default GET
headers: { Authorization: "Bearer …" },
body: { any: "json" }, // or a string
timeoutMs: 60000,
});
res.status; res.ok; res.headers; res.text(); res.json();
// Images and other binary files
const img = await octopus.http.fetch("https://host/chart.png", { binary: true });
el.src = img.dataUrl();
Claude
Claude Code without a terminal, in the Claude account of the project you're working on. It reads the extension's own files and uses skills, but can't run commands, edit files or reach the web: give it the data. An answer takes 5 to 30 seconds, so show progress; "haiku" and "sonnet" answer faster. Ask for JSON when you need data.
const answer = await octopus.claude.ask("Group these errors into patterns, as JSON: …", { model: "sonnet" });
// A conversation: each turn continues the last.
let { text, session } = await octopus.claude.chat("…", { system: "You are…" });
({ text, session } = await octopus.claude.chat("and then?", { session }));
// A full Claude Code session in a project's terminals, started with this prompt. It resolves with
// the terminal's id; background: true keeps the extension in front.
const termId = await octopus.claude.open("Investigate the 502s on checkout", { projectId, title: "502s", background: true });
To show that session inside your page, see Claude in your page.
Projects and tasks
const projects = await octopus.projects.list(); // [{ id, name, path, color }]
const tasks = await octopus.tasks.list({ projectId });
// [{ id, projectId, title, status, priority, tags, blocked, blockedReason, archived }]
const task = await octopus.tasks.create({ title, body: "Markdown", projectId, priority: 2, tags: ["logs"] });
await octopus.tasks.update(task.id, { status: "doing", blocked: true, blockedReason: "Waiting for QA" });
await octopus.tasks.update(task.id, { status: "done", archived: true });
await octopus.tasks.reveal(task.id); // shows it on its board
status is todo, doing, review or done; priority goes from 0 (low) to 3 (urgent). Without projectId, list returns every project's tasks. update changes only what you give it (title, body, status, priority, tags, blocked and blockedReason, archived), and only a done task can be archived.
Git
Open a branch's review in the project's Git tab: everything it changes against the branch it goes into, as a merge request shows it. No permission needed, since only you see the code. By default: the project you're working on, its checked-out branch and the remote's default branch.
const { branch, target } = await octopus.git.review({ projectId, branch: "feature/dark-mode", target: "main" });
Terminals
const terms = await octopus.terminals.list();
// [{ id, projectId, taskId, title, kind: "claude" | "shell", state, exitCode, startedAt, lastOutputAt, floating }]
const text = await octopus.terminals.read(terms[0].id, { lines: 300 });
octopus.on("terminals", (list) => {}); // opened, closed, or changed state
await octopus.terminals.reveal(id); // shows it in its project's Terminals, focused
state is working, idle, waiting (it's waiting for you) or ended.
Claude in your page
A Claude session your extension opened can be shown right inside its page: Octopus draws its own terminal over an element of yours and follows it as it moves, resizes, shows or hides. You type in it as in Terminals; the page still can't. When the page lets it go, the session goes back to its project's Terminals.
const termId = await octopus.claude.open("Review the failing tests", { projectId, background: true });
const view = await octopus.terminals.embed(termId, document.getElementById("term"));
view.focus();
view.dispose(); // back to Terminals
await octopus.terminals.close(termId); // ends it: only sessions this extension opened
It works in the extension's section, not in a window of its own.
Documentation
A report and the charts it shows, written in a project's documentation folder (default: the project you're working on). An existing file is kept unless you pass overwrite: true.
const path = await octopus.docs.write("reports/2026-10-checkout.md", markdown, { projectId });
await octopus.docs.write("reports/volume.svg", svg, { projectId });
await octopus.docs.open(path, { projectId }); // in the Documentation tab
Storage and sharing
await octopus.storage.set("savedQueries", [...]); // JSON, kept by Octopus
const saved = await octopus.storage.get("savedQueries"); // null if never set
// Publish data for other extensions, and read theirs (permissions.shared).
await octopus.shared.set("events", [...]);
const events = await octopus.shared.get("calendar", "events");
octopus.on("shared", ({ extension, key }) => {});
const exts = await octopus.extensions.list(); // no permission needed
Small things
octopus.toast("Saved", "success"); // info, success, error
octopus.copy(text); // to the clipboard
octopus.openUrl("https://…"); // in the browser
octopus.on("theme", (theme) => {}); // colours already applied
octopus.on("visible", (shown) => {}); // the section was shown or hidden
A website as an extension
To bring a site itself into Octopus (Slack, WhatsApp Web, your dashboards) instead of a page of your own, set permissions.webapp to its address. Octopus shows it in a view of its own, keeps you signed in, and counts what's unread from its title.
{
"name": "Chat",
"icon": "comment",
"permissions": {
"webapp": [
{ "id": "slack", "name": "Slack", "url": "https://app.slack.com/client", "color": "#4A154B" },
{ "id": "telegram", "name": "Telegram", "url": "https://web.telegram.org/a/", "color": "#229ED9" }
]
}
}
A single site can be a plain address: "webapp": "https://web.whatsapp.com". With several, you pin the ones you use as tabs. Add "userAgent": "chrome" for a site that says Safari isn't supported. The site can't reach Octopus, and Octopus can't read the site's content.
Extensions that need others
An extension can build on another: a standup that reads the meetings a calendar extension shares, say. Name them in dependencies, each with the oldest version that will do:
{
"name": "Standup",
"permissions": { "shared": ["calendar"] },
"dependencies": { "calendar": "1.2.0", "jira": "*" }
}
"*" takes any version. A plain list of ids works too, ["calendar", "jira"], and so does [{ "id": "calendar", "version": "1.2.0" }].
- Installing. Octopus asks once, listing what it will install, update or turn on first, looking in the extension's own marketplace before the others. Then it installs them in order, and the extension last. If one can't be found, or no version is new enough, it says so before anything changes.
- In its details. An extension shows what it needs and which installed extensions need it.
- Uninstalling or turning one off. If others need it, Octopus says which before it goes.
A dependency can still be missing or turned off, for example in an extension you copied by hand. Check octopus.extensions.list() and tell the user what's needed rather than failing silently.
Rules and limits
- No build step. HTML, CSS and ES modules loaded as they are. No npm, no bundler.
- No CDN. The page runs under a Content Security Policy that allows only its own files. A library is fine as a file copied into the folder, like
vendor/chart.umd.js. - No network but
octopus.http.fetch. A plainfetch()to another host fails, and so do remote images: fetch them withbinary: trueand usedataUrl(). - Only its own folder. An extension never changes Octopus itself: the SDK is all it gets.
- Storage that lasts.
localStorageworks, butoctopus.storageis the one Octopus keeps.
Ready to build yours?
Download Octopus, open Extensions and press Create with Claude, or start from the Standup tutorial above.
Download Octopus