Claudinho

unlisted

by gadfaria

Claude dentro do Obsidian: chat lateral, comandos sobre a seleção e edição agêntica do vault.

Updated 1mo ago
View on GitHub

Claudinho

Claude inside Obsidian, running through the local Claude Agent SDK (it uses your Claude Code subscription, not an API key). Side chat, commands over the selection, and agentic vault editing with approval.

Desktop only.

How the build works

The plugin is self-contained: main.js (~1.7 MB) bundles the Agent SDK and zod, plus manifest.json and styles.css. No node_modules needed in the vault.

Three traps the build works around, which break if you touch esbuild.config.mjs:

  1. The SDK is ESM-only and the Obsidian plugin is CJS. Loading sdk.mjs through a dynamic import() does not work: in the Electron renderer, import() is the browser loader, which tries to fetch file:// and fails with "Failed to fetch dynamically imported module". That's why esbuild converts the SDK to CJS along with our own code.
  2. The SDK calls createRequire(import.meta.url) internally. During the CJS conversion, esbuild would leave that undefined and the require would blow up on load. The config injects the URL of main.js itself at runtime via define + banner.
  3. new AbortController() in the renderer returns the DOM AbortController. The SDK passes .signal to Node's setMaxListeners, which validates by instanceof and rejects it (The "eventTargets" argument must be an instance of EventEmitter or EventTarget). src/shims/events.cjs covers that case, and esbuild's alias applies the shim only inside our bundle. The alias targets events without the prefix: the shim reaches the real module through node:events, which stays external — if both were aliased, the shim would reference itself.

Usage

  • Referencing notes: type @ in the chat and pick the note. The search is fuzzy over the path; with nothing typed, it lists the most recently opened ones. ↑↓ navigates, Enter/Tab picks, Esc closes. Enter only sends the message when the list is closed.
  • / commands: type / at the start of a message. The list has two families: the plugin's local ones (/model opus, /clear, /thinking, /reads, /help), which never reach the agent, and the native Claude Code ones (/context, /compact, /cost…), which are passed through as text because the CLI itself interprets them. The native ones only show up in the list after the first message, once the session init announces which exist.
  • Switching models: the selector in the chat header. It works mid-conversation (Query.setModel) — the context is preserved and the next turn already runs on the new model.

Commands

npm install
npm run build     # typecheck + bundle
npm run deploy    # build + copies the 3 files to the vault
npm run dev       # bundle with sourcemap, no typecheck

The deploy target comes from OBSIDIAN_VAULT or the default in scripts/deploy.mjs. After deploying: ⌘P in Obsidian → Reload app without saving.

Structure

FileRole
src/shims/events.cjsMakes setMaxListeners tolerate the DOM AbortSignal
src/agent/binary.tsFinds the claude executable (Obsidian's PATH lacks ~/.local/bin)
src/agent/session.tsLong-lived session with streaming input, permissions and events
src/agent/tools.tsIn-process MCP: active note, selection, tags, backlinks
src/agent/prompt.tsReframes the claude_code preset for a note vault
src/ui/chat-view.tsSide panel
src/ui/approval-card.tsApproval gate and AskUserQuestion
src/ui/suggest.tsGeneric autocomplete popup for the textarea
src/ui/mentions.ts@note autocomplete and reference resolution
src/ui/slash.tsLocal / commands and passthrough of the native ones
src/commands/selection.tsOne-shot commands over the selection

Permission model

  • Reading (Read, Glob, Grep) is auto-approved by default — configurable.
  • Writing (Write, Edit) and Bash always go through the approval card, with a diff.
  • settingSources: []: the agent doesn't inherit your CLAUDE.md or global settings.
☕ Code and Coffee

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.