External audit flagged 8 actionable items against current best practices for Agent Skills, Claude Code hooks, Obsidian v1.9-v1.12, and JSON Canvas 1.0. This release addresses all of them and adds multi-agent compatibility files at zero cost to existing users. Tier 1, critical fixes: * wiki/meta/dashboard.base: NEW Obsidian Bases dashboard (native, no plugin needed). Replaces Dataview as the primary dashboard. Six views: Recent Activity, Seed Pages, Entities Missing Sources, Open Questions, Comparisons, Sources. * wiki/meta/dashboard.md: now embeds dashboard.base. Legacy Dataview queries retained as optional fallback for users on Obsidian < 1.9.10. * README.md and skills/wiki/references/plugins.md: Plugins section reorganized to recommend Bases (core, no install) primary, Dataview optional/legacy. * skills/canvas/references/canvas-spec.md: added missing JSON Canvas 1.0 fields. Group nodes now document background and backgroundStyle (cover, ratio, repeat). Edges document fromEnd (default 'none') and toEnd (default 'arrow') asymmetric defaults. Hex ID convention noted alongside descriptive ID alternative. * .gitignore: track wiki/meta/dashboard.base explicitly. Tier 2, important improvements: * hooks/hooks.json: SessionStart now uses both command type ([ -f wiki/hot.md ] && cat ...) and prompt type. Command type is the canonical safety check that works in non-vault sessions without erroring. Matcher: startup|resume. * hooks/hooks.json: NEW PostCompact hook re-injects hot cache after context compaction (hook-injected context does not survive compaction; CLAUDE.md does). * hooks/hooks.json: PostToolUse auto-commit now guarded by [ -d .git ]. * hooks/README.md: NEW documentation including known plugin-hooks STDOUT bug (anthropics/claude-code#10875) and workarounds. * skills/wiki/references/mcp-setup.md: added Option D (Obsidian CLI) for v1.12+. Added warning callout above NODE_TLS_REJECT_UNAUTHORIZED line explaining process-wide TLS bypass and recommending Option D as the secure alternative. * skills/wiki-ingest/SKILL.md: documented [!contradiction] custom callout CSS dependency on vault-colors.css snippet. * skills/wiki/references/css-snippets.md: full documentation of all four custom callouts (contradiction, gap, key-insight, stale) with built-in fallback equivalents. Tier 3, multi-agent compatibility (low complexity, high reach): * AGENTS.md: Codex CLI / OpenCode bootstrap. * GEMINI.md: Gemini CLI / Antigravity bootstrap. * .cursor/rules/claude-obsidian.mdc: Cursor always-on rules. * .windsurf/rules/claude-obsidian.md: Windsurf Cascade rules. * .github/copilot-instructions.md: GitHub Copilot conventions. * bin/setup-multi-agent.sh: idempotent symlink installer for Codex, OpenCode, Gemini, Cursor, Windsurf. Wires up the skills/ directory in each agent's expected location. Style cleanup: scrubbed all em dashes from every skill, hook, doc, and bootstrap file (249 total replacements across 26 files). Skills now use periods, commas, and colons throughout for cleaner natural prose. Version: 1.3.0 to 1.4.0 (aligns plugin.json with GitHub release tag format). Already resolved in v1.1 (no action needed): * defuddle, obsidian-bases, obsidian-markdown skills shipped * URL ingestion, vision ingestion, delta tracking docs * Multi-depth wiki-query (Quick / Standard / Deep) * PostToolUse auto-commit hook * allowed-tools field removed from all SKILL.md files * All templates already use plural tag/alias forms * Custom callouts CSS already in vault-colors.css Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
293 lines
8.5 KiB
Markdown
293 lines
8.5 KiB
Markdown
# Obsidian Canvas JSON Specification
|
||
|
||
Canvas files are JSON with two top-level keys: `nodes` (array) and `edges` (array).
|
||
Obsidian reads and writes them as UTF-8 JSON files with `.canvas` extension.
|
||
|
||
This reference aligns with the [JSON Canvas 1.0 open specification](https://jsoncanvas.org/spec/1.0/). All structures support arbitrary additional fields (`[key: string]: any`) for forward compatibility. Obsidian will preserve unknown fields when reading and writing canvas files.
|
||
|
||
**ID format**: The JSON Canvas 1.0 spec recommends 16-character lowercase hexadecimal IDs (e.g., `"a1b2c3d4e5f67890"`). Obsidian itself generates IDs in this format. The descriptive ID examples in this reference (`"text-title-4821"`, `"img-cover-7823"`) are an alternative naming convention that this plugin uses for human readability. Both are valid JSON Canvas. Use whichever fits your workflow.
|
||
|
||
---
|
||
|
||
## Coordinate System
|
||
|
||
```
|
||
x increases →
|
||
┌─────────────────────────────────
|
||
│ (-920, -2400) (0, -2400)
|
||
│
|
||
y │ (-920, 0) (0, 0) ← origin
|
||
↓ │
|
||
│ (-920, 540) (500, 540)
|
||
```
|
||
|
||
- **Origin** (0, 0) is the center of the canvas viewport.
|
||
- **x increases rightward.** Negative x = left of center.
|
||
- **y increases downward.** Negative y = above center.
|
||
- Node `x` and `y` are the **top-left corner** of the node, not the center.
|
||
- Obsidian pans to fit all nodes on first open.
|
||
|
||
---
|
||
|
||
## Node Types
|
||
|
||
### Text node
|
||
|
||
Renders markdown content as a styled card.
|
||
|
||
```json
|
||
{
|
||
"id": "text-title-4821",
|
||
"type": "text",
|
||
"text": "# Heading\n\nParagraph with **bold** and `code`.",
|
||
"x": -400,
|
||
"y": -300,
|
||
"width": 400,
|
||
"height": 120,
|
||
"color": "6"
|
||
}
|
||
```
|
||
|
||
- `text`: markdown string. Use `\n` for newlines.
|
||
- Minimum readable size: width ≥ 200, height ≥ 60.
|
||
- `color` is optional. Omit for default (no color).
|
||
|
||
---
|
||
|
||
### File node
|
||
|
||
Renders an image, PDF, markdown note, or other vault file inline.
|
||
|
||
```json
|
||
{
|
||
"id": "img-cover-7823",
|
||
"type": "file",
|
||
"file": "_attachments/images/example.png",
|
||
"x": -900,
|
||
"y": -100,
|
||
"width": 420,
|
||
"height": 236
|
||
}
|
||
```
|
||
|
||
- `file`: **vault-relative path** (not absolute, not `~/`).
|
||
- Supported: `.png` `.jpg` `.webp` `.gif` `.pdf` `.md` `.canvas`
|
||
- For `.md` files: renders as a preview card.
|
||
- For `.pdf` files: renders the first page as preview.
|
||
- No `color` field for file nodes: color is ignored.
|
||
|
||
---
|
||
|
||
### Group node (Zone)
|
||
|
||
A labeled rectangular region. Does not clip or contain nodes. It's a visual guide.
|
||
Nodes placed "inside" a group are just positioned within its bounding box.
|
||
|
||
```json
|
||
{
|
||
"id": "zone-branding-3391",
|
||
"type": "group",
|
||
"label": "Brand Identity",
|
||
"x": -920,
|
||
"y": -880,
|
||
"width": 1060,
|
||
"height": 290,
|
||
"color": "6",
|
||
"background": "_attachments/images/grid-bg.png",
|
||
"backgroundStyle": "cover"
|
||
}
|
||
```
|
||
|
||
- `label`: shown at the top of the group box.
|
||
- `color`: colors the group border and label.
|
||
- `background` *(optional)*: vault-relative path to a background image for the group.
|
||
- `backgroundStyle` *(optional)*: how the background is rendered.
|
||
- `"cover"`: fills the group, cropping if needed (default-ish behavior)
|
||
- `"ratio"`: preserves aspect ratio, fits inside the group
|
||
- `"repeat"`: tiles the image
|
||
- Groups do not affect auto-layout: they are purely visual containers.
|
||
|
||
---
|
||
|
||
### Link node
|
||
|
||
Renders a web URL as an embedded preview card.
|
||
|
||
```json
|
||
{
|
||
"id": "link-karpathy-2233",
|
||
"type": "link",
|
||
"url": "https://github.com/karpathy",
|
||
"x": 200,
|
||
"y": -300,
|
||
"width": 400,
|
||
"height": 120
|
||
}
|
||
```
|
||
|
||
- `url`: must be a valid `https://` URL.
|
||
- Obsidian fetches the Open Graph preview (title, description, thumbnail).
|
||
|
||
---
|
||
|
||
## Edges
|
||
|
||
Connections between nodes. Usually empty for mood boards.
|
||
|
||
```json
|
||
{
|
||
"id": "e-hub-cidx",
|
||
"fromNode": "hub",
|
||
"fromSide": "right",
|
||
"fromEnd": "none",
|
||
"toNode": "c-idx",
|
||
"toSide": "left",
|
||
"toEnd": "arrow",
|
||
"label": "concepts",
|
||
"color": "5"
|
||
}
|
||
```
|
||
|
||
**Required fields**: `id`, `fromNode`, `toNode`. Everything else is optional.
|
||
|
||
- `fromNode` / `toNode`: IDs of the source and target nodes.
|
||
- `fromSide` / `toSide` *(optional)*: `"top"` `"bottom"` `"left"` `"right"`. If omitted, Obsidian auto-calculates the best side based on relative node positions.
|
||
- `fromEnd` *(optional)*: end-cap on the source side. Defaults to `"none"`. Values: `"none"` | `"arrow"`.
|
||
- `toEnd` *(optional)*: end-cap on the target side. **Defaults to `"arrow"`**: note the asymmetric default vs `fromEnd`. Values: `"none"` | `"arrow"`.
|
||
- `label` *(optional)*: text shown on the edge.
|
||
- `color` *(optional)*: same color palette as nodes (`"1"`–`"6"` or hex).
|
||
|
||
Most edges represent directed relationships, so the asymmetric defaults (`fromEnd: "none"`, `toEnd: "arrow"`) produce a single arrow pointing from source to target without specifying anything explicitly.
|
||
|
||
---
|
||
|
||
## Color Reference
|
||
|
||
| Code | Color | Hex (approx) | Use case |
|
||
|------|-------|-------------|----------|
|
||
| `"1"` | Red / Tomato | #e03e3e | Warnings, archive |
|
||
| `"2"` | Orange | #d09035 | Active work |
|
||
| `"3"` | Yellow / Gold | #d0a023 | WIP, notes |
|
||
| `"4"` | Green / Teal | #448361 | Content, sources |
|
||
| `"5"` | Blue / Cyan | #3ea7d3 | Navigation, info |
|
||
| `"6"` | Purple / Violet | #9063d2 | Title, identity |
|
||
|
||
Omit `color` entirely for the default (no border color, transparent label).
|
||
|
||
---
|
||
|
||
## Image Sizing Guidelines
|
||
|
||
Calculate from actual image dimensions using PIL or `identify`:
|
||
|
||
```bash
|
||
python3 -c "from PIL import Image; img=Image.open('path.png'); print(img.width, img.height)"
|
||
# or
|
||
identify -format '%w %h' path.png
|
||
```
|
||
|
||
| Aspect ratio | Condition | Canvas width | Canvas height |
|
||
|-------------|-----------|-------------|--------------|
|
||
| 16:9 (wide) | ratio 1.6–2.0 | 420 | 236 |
|
||
| 2:1 (ultra wide) | ratio > 2.0 | 440 | 220 |
|
||
| 4:3 | ratio 1.2–1.6 | 380 | 285 |
|
||
| 1:1 (square) | ratio 0.9–1.1 | 280 | 280 |
|
||
| 3:4 | ratio 0.6–0.9 | 240 | 320 |
|
||
| 9:16 (portrait) | ratio < 0.6 | 200 | 356 |
|
||
| PDF | any | 400 | 520 |
|
||
| Unknown | fallback | 320 | 240 |
|
||
|
||
---
|
||
|
||
## Auto-Positioning Pseudocode
|
||
|
||
```
|
||
function place_node(canvas, zone_label, new_w, new_h):
|
||
zone = find group node where label == zone_label
|
||
padding = 20
|
||
|
||
if zone not found:
|
||
max_y = max(n.y + n.height for n in canvas.nodes) + 60
|
||
return (-400, max_y)
|
||
|
||
# Nodes visually inside zone
|
||
inside = [n for n in canvas.nodes
|
||
if n.type != 'group'
|
||
and zone.x <= n.x < zone.x + zone.width
|
||
and zone.y <= n.y < zone.y + zone.height]
|
||
|
||
if inside is empty:
|
||
return (zone.x + padding, zone.y + padding)
|
||
|
||
# Rightmost point in zone
|
||
rightmost = max(n.x + n.width for n in inside)
|
||
next_x = rightmost + 40
|
||
|
||
if next_x + new_w > zone.x + zone.width - padding:
|
||
# Overflow → new row
|
||
bottom_of_row = max(n.y + n.height for n in inside)
|
||
return (zone.x + padding, bottom_of_row + padding)
|
||
|
||
# Same row
|
||
row_y = min(n.y for n in inside) # align to top of existing row
|
||
return (next_x, row_y)
|
||
```
|
||
|
||
---
|
||
|
||
## Full Example: Two-Zone Canvas
|
||
|
||
```json
|
||
{
|
||
"nodes": [
|
||
{
|
||
"id": "title-0001",
|
||
"type": "text",
|
||
"text": "# Brand Reference\n\n**AI Marketing Hub** visual assets",
|
||
"x": -920, "y": -2440, "width": 560, "height": 180, "color": "6"
|
||
},
|
||
{
|
||
"id": "zone-logos",
|
||
"type": "group",
|
||
"label": "Logos & Icons",
|
||
"x": -920, "y": -2200, "width": 1800, "height": 320, "color": "6"
|
||
},
|
||
{
|
||
"id": "img-logo-pro",
|
||
"type": "file",
|
||
"file": "_attachments/images/example.png",
|
||
"x": -900, "y": -2180, "width": 420, "height": 236
|
||
},
|
||
{
|
||
"id": "img-icon-free",
|
||
"type": "file",
|
||
"file": "_attachments/images/example-icon.png",
|
||
"x": -440, "y": -2180, "width": 280, "height": 280
|
||
},
|
||
{
|
||
"id": "zone-covers",
|
||
"type": "group",
|
||
"label": "Skill Covers",
|
||
"x": -920, "y": -1820, "width": 1800, "height": 340, "color": "3"
|
||
},
|
||
{
|
||
"id": "img-seo",
|
||
"type": "file",
|
||
"file": "_attachments/images/example-cover.png",
|
||
"x": -900, "y": -1800, "width": 420, "height": 236
|
||
}
|
||
],
|
||
"edges": []
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Common Mistakes
|
||
|
||
- **Wrong path format**: use `_attachments/images/file.png` not `/home/user/...` or `~/...`
|
||
- **ID collision**: always read existing IDs before generating a new one
|
||
- **Negative y confusion**: `y: -2400` is ABOVE `y: -1000` (more negative = higher up)
|
||
- **Group does not clip**: placing a node "inside" a group is just positioning it within the group's bounding box: there is no parent-child relationship in the JSON
|
||
- **Missing height on text nodes**: Obsidian will render the text but may clip it if height is too small. Use height ≥ content-lines × 24.
|