diff --git a/docs/development/guides/bundled-skills.md b/docs/development/guides/bundled-skills.md
index 5f900755..599465cb 100644
--- a/docs/development/guides/bundled-skills.md
+++ b/docs/development/guides/bundled-skills.md
@@ -6,12 +6,12 @@ sidebar_position: 4
# Bundled Skills
-HybridClaw currently ships with 33 bundled skills. A few notable categories:
+HybridClaw currently ships with 34 bundled skills. A few notable categories:
- office workflows: `pdf`, `xlsx`, `docx`, `pptx`, `office-workflows`
- planning and engineering: `project-manager`, `feature-planning`,
`code-review`, `code-simplification`
-- visual explainers and animation: `manim-video`
+- visual explainers and animation: `manim-video`, `excalidraw`
- platform integrations: `github-pr-workflow`, `notion`, `trello`, `stripe`,
`wordpress`, `google-workspace`, `discord`
- knowledge workflows: `llm-wiki`, `obsidian`, `zettelkasten`
diff --git a/docs/index.html b/docs/index.html
index a48815e6..96745ab1 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1431,7 +1431,7 @@
Agent Packaging
diff --git a/skills/excalidraw/SKILL.md b/skills/excalidraw/SKILL.md
new file mode 100644
index 00000000..2c7901dc
--- /dev/null
+++ b/skills/excalidraw/SKILL.md
@@ -0,0 +1,146 @@
+---
+name: excalidraw
+description: Create and revise editable `.excalidraw` diagrams as Excalidraw JSON for architecture diagrams, flowcharts, sequence diagrams, concept maps, and other hand-drawn explainers.
+user-invocable: true
+disable-model-invocation: false
+metadata:
+ hybridclaw:
+ category: publishing
+ short_description: "Editable Excalidraw diagrams and share links."
+ tags:
+ - excalidraw
+ - diagrams
+ - flowcharts
+ - architecture
+ - visualization
+ related_skills:
+ - manim-video
+ - write-blog-post
+---
+# Excalidraw
+
+Use this skill when the user wants an editable diagram, not just a rendered image.
+
+Typical requests:
+
+- architecture or system diagrams
+- flowcharts and process maps
+- sequence diagrams
+- concept maps and explainers
+- hand-drawn style visuals that should stay editable in Excalidraw
+
+Excalidraw files are plain JSON. The default deliverable is a `*.excalidraw` file in the workspace. The user can drag that file into [excalidraw.com](https://excalidraw.com) to view, edit, or export it.
+
+## Default Workflow
+
+1. Plan the diagram before writing JSON: title, nodes, connectors, groups, and rough canvas size.
+2. Write a valid Excalidraw `elements` array.
+3. Wrap the array in the standard file envelope.
+4. Save the result as `*.excalidraw`.
+5. If the user wants a shareable browser link, run:
+
+```bash
+node skills/excalidraw/scripts/upload.mjs diagram.excalidraw
+```
+
+The upload helper encrypts the diagram client-side and prints the Excalidraw share URL.
+
+## File Envelope
+
+Use this shape unless you are editing an existing file and need to preserve more fields:
+
+```json
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "hybridclaw",
+ "elements": [],
+ "appState": {
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
+```
+
+When editing an existing `.excalidraw` file, preserve `appState`, `files`, and any other existing top-level keys unless the user asked for a deliberate reset.
+
+## Rules
+
+- Use Excalidraw JSON, not SVG or HTML, unless the user explicitly asked for another format.
+- For labeled shapes or arrows, create a separate `text` element and bind it with `containerId` plus the container's `boundElements`.
+- Do **not** invent a `"label"` property on rectangles, diamonds, ellipses, or arrows. Excalidraw ignores it.
+- Place a bound text element immediately after its container in the `elements` array.
+- Use readable sizes: `fontSize` 16+ for normal labels, 20+ for titles, and at least `120x60` for labeled boxes.
+- Leave about `20-30px` of space between major elements.
+- Prefer short stable ids such as `api`, `text-api`, `arrow-api-db`.
+- Avoid emoji and decorative Unicode. Stick to plain text that Excalidraw renders reliably.
+- Default to a white background with dark text unless the user explicitly asks for dark mode.
+- For arrows, `points` are offsets relative to the arrow's `x` and `y`.
+
+## Core Patterns
+
+### Labeled Rectangle
+
+```json
+[
+ {
+ "type": "rectangle",
+ "id": "api",
+ "x": 120,
+ "y": 120,
+ "width": 220,
+ "height": 80,
+ "roundness": { "type": 3 },
+ "backgroundColor": "#a5d8ff",
+ "fillStyle": "solid",
+ "boundElements": [{ "id": "text-api", "type": "text" }]
+ },
+ {
+ "type": "text",
+ "id": "text-api",
+ "x": 130,
+ "y": 145,
+ "width": 200,
+ "height": 24,
+ "text": "API Service",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "api",
+ "originalText": "API Service",
+ "autoResize": true
+ }
+]
+```
+
+### Arrow Between Shapes
+
+```json
+{
+ "type": "arrow",
+ "id": "arrow-api-db",
+ "x": 340,
+ "y": 160,
+ "width": 180,
+ "height": 0,
+ "points": [[0, 0], [180, 0]],
+ "endArrowhead": "arrow",
+ "startBinding": { "elementId": "api", "fixedPoint": [1, 0.5] },
+ "endBinding": { "elementId": "db", "fixedPoint": [0, 0.5] }
+}
+```
+
+## Reference Files
+
+- For palette and contrast guidance, read [references/colors.md](references/colors.md).
+- For copy-pasteable diagram patterns, read [references/examples.md](references/examples.md).
+- For dark-background diagrams, read [references/dark-mode.md](references/dark-mode.md).
+
+## Anti-Patterns
+
+- Do not cram many tiny nodes into one canvas when two simpler diagrams would read better.
+- Do not put all shapes first and all text last; that usually breaks layering and bindings.
+- Do not guess at Excalidraw-only properties you have not already seen in a working example.
+- Do not replace an editable diagram request with a static PNG export unless the user asked for the export.
diff --git a/skills/excalidraw/agents/openai.yaml b/skills/excalidraw/agents/openai.yaml
new file mode 100644
index 00000000..7dcafd54
--- /dev/null
+++ b/skills/excalidraw/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "Excalidraw"
+ short_description: "Editable Excalidraw diagrams and share links."
+ default_prompt: "Use $excalidraw to create an editable Excalidraw diagram for this system or process and save the .excalidraw file in the workspace."
diff --git a/skills/excalidraw/references/colors.md b/skills/excalidraw/references/colors.md
new file mode 100644
index 00000000..4d47d777
--- /dev/null
+++ b/skills/excalidraw/references/colors.md
@@ -0,0 +1,45 @@
+# Excalidraw Palette
+
+Use a small, consistent palette. Excalidraw diagrams look better when color meaning stays stable across the canvas.
+
+## Fill Colors
+
+| Use | Fill | Hex |
+|-----|------|-----|
+| Primary nodes | Light Blue | `#a5d8ff` |
+| Outputs / success | Light Green | `#b2f2bb` |
+| Warnings / external systems | Light Orange | `#ffd8a8` |
+| Processing / orchestration | Light Purple | `#d0bfff` |
+| Errors / critical states | Light Red | `#ffc9c9` |
+| Notes / decisions | Light Yellow | `#fff3bf` |
+| Storage / data | Light Teal | `#c3fae8` |
+
+## Stroke And Accent Colors
+
+| Use | Stroke | Hex |
+|-----|--------|-----|
+| Default outline / text | Dark Gray | `#1e1e1e` |
+| Blue accent | Blue | `#4a9eed` |
+| Green accent | Green | `#22c55e` |
+| Orange accent | Amber | `#f59e0b` |
+| Red accent | Red | `#ef4444` |
+| Purple accent | Purple | `#8b5cf6` |
+
+## Background Zones
+
+For layered diagrams, use large low-opacity rectangles behind content:
+
+| Layer | Color | Hex |
+|-------|-------|-----|
+| UI / frontend | Blue zone | `#dbe4ff` |
+| Logic / agent layer | Purple zone | `#e5dbff` |
+| Data / tools layer | Green zone | `#d3f9d8` |
+
+Use `opacity: 30-35` for those background zones so they do not overpower the main nodes.
+
+## Contrast Rules
+
+- On white backgrounds, prefer `#1e1e1e` for text.
+- Secondary text on white should stay at or darker than `#757575`.
+- Do not use pale gray text on white.
+- On light fills, keep text dark; do not match the fill color with bright text.
diff --git a/skills/excalidraw/references/dark-mode.md b/skills/excalidraw/references/dark-mode.md
new file mode 100644
index 00000000..fa918d80
--- /dev/null
+++ b/skills/excalidraw/references/dark-mode.md
@@ -0,0 +1,37 @@
+# Dark Mode
+
+For dark diagrams, add a large background rectangle as the first element in the array:
+
+```json
+{
+ "type": "rectangle",
+ "id": "dark-bg",
+ "x": -4000,
+ "y": -3000,
+ "width": 10000,
+ "height": 7500,
+ "backgroundColor": "#1e1e2e",
+ "fillStyle": "solid",
+ "strokeColor": "transparent",
+ "strokeWidth": 0
+}
+```
+
+## Text On Dark Backgrounds
+
+- Primary text: `#e5e5e5`
+- Secondary text: `#a0a0a0`
+- Do not use the default `#1e1e1e` text color on a dark background.
+
+## Useful Dark Fills
+
+| Use | Fill | Hex |
+|-----|------|-----|
+| Primary nodes | Dark Blue | `#1e3a5f` |
+| Success / output | Dark Green | `#1a4d2e` |
+| Processing | Dark Purple | `#2d1b69` |
+| Warning | Dark Orange | `#5c3d1a` |
+| Error | Dark Red | `#5c1a1a` |
+| Storage | Dark Teal | `#1a4d4d` |
+
+Keep bright stroke colors for arrows and borders so the diagram still reads clearly.
diff --git a/skills/excalidraw/references/examples.md b/skills/excalidraw/references/examples.md
new file mode 100644
index 00000000..9e9e1591
--- /dev/null
+++ b/skills/excalidraw/references/examples.md
@@ -0,0 +1,266 @@
+# Examples
+
+These are complete `elements` arrays. Wrap them in the `.excalidraw` envelope from `SKILL.md` before saving.
+
+## Example 1: Simple Flow
+
+```json
+[
+ {
+ "type": "text",
+ "id": "title",
+ "x": 270,
+ "y": 40,
+ "text": "Simple Flow",
+ "fontSize": 28,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "originalText": "Simple Flow",
+ "autoResize": true
+ },
+ {
+ "type": "rectangle",
+ "id": "start",
+ "x": 100,
+ "y": 130,
+ "width": 200,
+ "height": 90,
+ "roundness": { "type": 3 },
+ "backgroundColor": "#a5d8ff",
+ "fillStyle": "solid",
+ "boundElements": [{ "id": "text-start", "type": "text" }]
+ },
+ {
+ "type": "text",
+ "id": "text-start",
+ "x": 110,
+ "y": 160,
+ "width": 180,
+ "height": 24,
+ "text": "Start",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "start",
+ "originalText": "Start",
+ "autoResize": true
+ },
+ {
+ "type": "rectangle",
+ "id": "finish",
+ "x": 430,
+ "y": 130,
+ "width": 200,
+ "height": 90,
+ "roundness": { "type": 3 },
+ "backgroundColor": "#b2f2bb",
+ "fillStyle": "solid",
+ "boundElements": [{ "id": "text-finish", "type": "text" }]
+ },
+ {
+ "type": "text",
+ "id": "text-finish",
+ "x": 440,
+ "y": 160,
+ "width": 180,
+ "height": 24,
+ "text": "Finish",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "finish",
+ "originalText": "Finish",
+ "autoResize": true
+ },
+ {
+ "type": "arrow",
+ "id": "arrow-start-finish",
+ "x": 300,
+ "y": 175,
+ "width": 130,
+ "height": 0,
+ "points": [[0, 0], [130, 0]],
+ "endArrowhead": "arrow",
+ "startBinding": { "elementId": "start", "fixedPoint": [1, 0.5] },
+ "endBinding": { "elementId": "finish", "fixedPoint": [0, 0.5] }
+ }
+]
+```
+
+## Example 2: Client -> API -> Database
+
+```json
+[
+ {
+ "type": "text",
+ "id": "title",
+ "x": 210,
+ "y": 30,
+ "text": "Service Overview",
+ "fontSize": 28,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "originalText": "Service Overview",
+ "autoResize": true
+ },
+ {
+ "type": "rectangle",
+ "id": "client",
+ "x": 70,
+ "y": 150,
+ "width": 180,
+ "height": 80,
+ "roundness": { "type": 3 },
+ "backgroundColor": "#a5d8ff",
+ "fillStyle": "solid",
+ "boundElements": [{ "id": "text-client", "type": "text" }]
+ },
+ {
+ "type": "text",
+ "id": "text-client",
+ "x": 80,
+ "y": 178,
+ "width": 160,
+ "height": 24,
+ "text": "Web Client",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "client",
+ "originalText": "Web Client",
+ "autoResize": true
+ },
+ {
+ "type": "rectangle",
+ "id": "api",
+ "x": 330,
+ "y": 150,
+ "width": 200,
+ "height": 80,
+ "roundness": { "type": 3 },
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "boundElements": [{ "id": "text-api", "type": "text" }]
+ },
+ {
+ "type": "text",
+ "id": "text-api",
+ "x": 340,
+ "y": 178,
+ "width": 180,
+ "height": 24,
+ "text": "API Service",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "api",
+ "originalText": "API Service",
+ "autoResize": true
+ },
+ {
+ "type": "rectangle",
+ "id": "db",
+ "x": 620,
+ "y": 150,
+ "width": 190,
+ "height": 80,
+ "roundness": { "type": 3 },
+ "backgroundColor": "#c3fae8",
+ "fillStyle": "solid",
+ "boundElements": [{ "id": "text-db", "type": "text" }]
+ },
+ {
+ "type": "text",
+ "id": "text-db",
+ "x": 630,
+ "y": 178,
+ "width": 170,
+ "height": 24,
+ "text": "Database",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "db",
+ "originalText": "Database",
+ "autoResize": true
+ },
+ {
+ "type": "arrow",
+ "id": "arrow-client-api",
+ "x": 250,
+ "y": 190,
+ "width": 80,
+ "height": 0,
+ "points": [[0, 0], [80, 0]],
+ "endArrowhead": "arrow",
+ "boundElements": [{ "id": "text-http", "type": "text" }],
+ "startBinding": { "elementId": "client", "fixedPoint": [1, 0.5] },
+ "endBinding": { "elementId": "api", "fixedPoint": [0, 0.5] }
+ },
+ {
+ "type": "text",
+ "id": "text-http",
+ "x": 270,
+ "y": 162,
+ "width": 40,
+ "height": 20,
+ "text": "HTTP",
+ "fontSize": 16,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "arrow-client-api",
+ "originalText": "HTTP",
+ "autoResize": true
+ },
+ {
+ "type": "arrow",
+ "id": "arrow-api-db",
+ "x": 530,
+ "y": 190,
+ "width": 90,
+ "height": 0,
+ "points": [[0, 0], [90, 0]],
+ "endArrowhead": "arrow",
+ "boundElements": [{ "id": "text-sql", "type": "text" }],
+ "startBinding": { "elementId": "api", "fixedPoint": [1, 0.5] },
+ "endBinding": { "elementId": "db", "fixedPoint": [0, 0.5] }
+ },
+ {
+ "type": "text",
+ "id": "text-sql",
+ "x": 555,
+ "y": 162,
+ "width": 40,
+ "height": 20,
+ "text": "SQL",
+ "fontSize": 16,
+ "fontFamily": 1,
+ "strokeColor": "#1e1e1e",
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "arrow-api-db",
+ "originalText": "SQL",
+ "autoResize": true
+ }
+]
+```
+
+## Common Mistakes
+
+- Do not use `"label"` on shapes or arrows.
+- Do not forget `containerId` on bound text.
+- Do not forget `boundElements` on the container.
+- Do not use tiny labels or cramped node spacing.
+- Do not put bound text far away from the shape it belongs to in the array.
diff --git a/skills/excalidraw/scripts/upload.mjs b/skills/excalidraw/scripts/upload.mjs
new file mode 100644
index 00000000..f61a412a
--- /dev/null
+++ b/skills/excalidraw/scripts/upload.mjs
@@ -0,0 +1,94 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs';
+import { createCipheriv, randomBytes } from 'node:crypto';
+import { deflateSync } from 'node:zlib';
+
+const UPLOAD_URL = 'https://json.excalidraw.com/api/v2/post/';
+
+function concatBuffers(...buffers) {
+ const parts = [];
+ const version = Buffer.alloc(4);
+ version.writeUInt32BE(1, 0);
+ parts.push(version);
+
+ for (const buffer of buffers) {
+ const length = Buffer.alloc(4);
+ length.writeUInt32BE(buffer.length, 0);
+ parts.push(length, buffer);
+ }
+
+ return Buffer.concat(parts);
+}
+
+function validateDocument(document) {
+ if (!document || typeof document !== 'object' || Array.isArray(document)) {
+ throw new Error('File must contain a JSON object.');
+ }
+
+ if (!Array.isArray(document.elements)) {
+ throw new Error('File must contain an "elements" array.');
+ }
+}
+
+async function uploadExcalidrawJson(excalidrawJson) {
+ const fileMetadata = Buffer.from(JSON.stringify({}), 'utf8');
+ const dataBytes = Buffer.from(excalidrawJson, 'utf8');
+ const innerPayload = concatBuffers(fileMetadata, dataBytes);
+ const compressed = deflateSync(innerPayload);
+
+ const key = randomBytes(16);
+ const iv = randomBytes(12);
+ const cipher = createCipheriv('aes-128-gcm', key, iv);
+ const ciphertext = Buffer.concat([
+ cipher.update(compressed),
+ cipher.final(),
+ cipher.getAuthTag(),
+ ]);
+
+ const encodingMetadata = Buffer.from(
+ JSON.stringify({
+ version: 2,
+ compression: 'pako@1',
+ encryption: 'AES-GCM',
+ }),
+ 'utf8',
+ );
+ const payload = concatBuffers(encodingMetadata, iv, ciphertext);
+
+ const response = await fetch(UPLOAD_URL, {
+ method: 'POST',
+ body: payload,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Upload failed with HTTP ${response.status}`);
+ }
+
+ const result = await response.json();
+ if (!result?.id) {
+ throw new Error(`Upload returned no file id: ${JSON.stringify(result)}`);
+ }
+
+ return `https://excalidraw.com/#json=${result.id},${key.toString('base64url')}`;
+}
+
+async function main() {
+ const filePath = process.argv[2];
+ if (!filePath) {
+ throw new Error('Usage: node skills/excalidraw/scripts/upload.mjs
');
+ }
+
+ const content = fs.readFileSync(filePath, 'utf8');
+ const document = JSON.parse(content);
+ validateDocument(document);
+
+ const url = await uploadExcalidrawJson(content);
+ process.stdout.write(`${url}\n`);
+}
+
+main().catch((error) => {
+ const message = error instanceof Error ? error.message : String(error);
+ process.stderr.write(`Error: ${message}\n`);
+ process.exitCode = 1;
+});