Documentation

Installation & configuration

Requirements#

ObsidianDesktop, 1.7.2 or newer. Chips and automations spawn local processes, so there is no mobile version.
A coding agent CLIOn your PATH — Claude Code, Codex, or whichever one you point a tool command at. The boards render without one; the chips have nothing to launch.
Node.js 18 or newerOn your PATH, for the run lifecycle hooks and any automation you wire up. The plugin itself never shells out to it — it runs inside Obsidian.

Node is the only thing Dispatch asks of your repository, and it asks for the runtime alone: the scripts it ships are dependency-free ESM, so there is no package.json, no lockfile, no node_modules and no build step. Adopting Dispatch does not turn a Python or Rust repository into a JavaScript one.

The 18 floor comes from the optional Meet transcript import, which uses the global fetch. run-state.mjs and a typical automation script use nothing newer than the built-in fs and child_process modules and run on far older versions — one floor for all of them is simpler to state than three.

Anything past this is your project's choice rather than Dispatch's. This repository's own tracker sync shells out to gh; a project tracking work in Asana or Jira writes a different script with a different dependency, and Dispatch neither ships nor configures it.

Install#

Open Settings → Community plugins → Browse in Obsidian, search for Dispatch, then install and enable it. The directory listing is the same plugin.

Guided setup#

Integrating Dispatch into a project — boards, device config, chips, tracker sync, agent hooks — is itself agent-guided, in Claude Code or Codex. Install the setup skill into the agent you run.

Claude Code:

/plugin marketplace add kaimys/obsidian-dispatch
/plugin install dispatch-setup

Codex:

codex plugin marketplace add kaimys/obsidian-dispatch
codex plugin add dispatch-setup@dispatch

Then say "set up Dispatch for this project" in your repo — or let the board start it. An unconfigured board offers a setup button for every agent this device has a launch command for: Set up with Claude or Set up with Codex when there is one, Set up with an agent when there are several (the confirmation dialog asks which; with Confirm before running off, the button names the agent a click starts). With no launch command it is disabled, and Copy the prompt hands the same prompt to an agent you already have open. The prompt names both install routes, so an agent that does not have the skill yet tells you how to install it and to start a new session.

The skill scans an existing vault (ticket folders, status vocabulary, frontmatter fill rates) and turns the interview into a confirmation of pre-filled suggestions — or scaffolds a wiki structure if there isn't one yet, and proposes the workflow skills for your code repo.

The setup is complete only after its verification gate and one live chip have passed for every selected agent. In a multi-agent setup, the skill creates one neutral chip per workflow intent and keeps the agent-specific / or $ prefix in the device-local tool configuration, so the same menu entry can launch either agent.

Neither agent? The skill is a plain markdown checklist: plugins/dispatch-setup/skills/dispatch-setup/SKILL.md.

The two configuration layers#

Dispatch splits its settings so a vault can be shared across a team without leaking machine-specific paths:

LayerStored inSynced?Contains
Shareddata.json (normal plugin settings)yes, with the vaultfolders, properties, columns, chip templates, automation rules, default tool
This device~/.dispatch/<vault>-<hash>.json (user profile, outside the vault)neverrepo alias → absolute path, tool command templates, calendar URL, opt-in toggles

Notes and shared settings never contain absolute paths. They reference repositories by alias (e.g. my-project), and each team member maps that alias to a local path once in Settings → Dispatch → This device.

Because the device layer lives outside the vault (Windows: %USERPROFILE%\.dispatch\), it works with any sync — Obsidian Sync, Google Drive, git — without exclusion rules, and teammates can never overwrite each other's device config. The exact path is shown in the settings tab. A local.json from older versions found next to the plugin is migrated there and removed from the vault automatically.

Scripts Dispatch ships — currently the Meet transcript import — keep their settings in that same per-vault file rather than one of their own, under a google key. A script the project chooses, such as a tracker sync, is configured by the project and Dispatch never writes it.

The Dispatch folder in your repository#

Everything Dispatch adds to a code repository sits in one folder, dispatch/: the workflow files (workflow/), the repo-side scripts (scripts/), the shared project invariants (invariants.md) and wiki, a git-ignored link to the vault. The folder also has room for project-level settings in settings.yaml; this repository keeps its tracker repository there, but the setup skill does not create the file yet and still writes the tracker into each workflow. It is committed and shared like the rest of the repository, so it never holds an absolute path, a secret or device state; those stay in ~/.dispatch/. The page templates stay in the vault, where Obsidian's Templates plugin can read them.

Set up before v0.3.0? Your project keeps its older layout (scripts/dispatch/, a root wiki link), and it keeps working after a plugin update: the plugin reads no path from your repository. Re-running the setup skill migrates it.

The sections below describe both layers as the settings UI presents them. If you (or an agent) write the files directly, read The config files on disk — the stored JSON does not have the same shape as the UI's compact input forms.

Board settings#

  • Source folders — vault folders scanned for cards (one per line)
  • Status property — the frontmatter property holding the column value (default status)
  • Order property — where the manual position within a column is stored (default rank; empty disables manual ordering)
  • Columns — ordered, one per line. Four segments: value | Display label | progress | WIP limit
    • progress (0–100) is the weight used by the Release Plan's progress bar; - excludes the status entirely (e.g. Rejected)
    • WIP limit makes the header show count/limit and outlines the column amber at the limit, red above
    • Statuses found in notes but not configured appear as extra columns at the end
  • Title / badge properties — what each card shows (e.g. id as title prefix, priority and type as badges)
  • Assignee property — shown as an accent-outlined @Name badge, always first in the slice-by bar
  • Open-questions property — numeric counter rendered as the ? N badge (amber → green at 0)
  • Open-tests property — numeric counter rendered as the ✓ N badge (purple → green at 0); open items in the manual test plan. Leave the property empty until the plan has been written — empty renders no badge, 0 claims a plan exists and every item is ticked
  • Open-findings property — numeric counter rendered as the ⚠ N badge (red → green at 0); blocking findings from the latest code review. Leave the property empty on a note nothing has reviewed — empty renders no badge, 0 claims the review found nothing
  • Discussion property — a thread URL rendered as a chat icon in the card title
  • Required properties — drives the ⚠ problems panel (typically id, status, updated)

Milestones (the Release Plan tab): version property, planned versions, per-version tags, release order property, size property, completed property, velocity look-back window, minimum completions, release-notes folder.

  • Release order property — where the manual build order inside a version column is stored (e.g. release_rank; empty, the default, keeps the columns sorted by status and turns off Copy release order to Kanban). Separate from the order property, so the Kanban and Release Plan orders never reorder each other.

The forecast's velocity comes from the completions inside the look-back window. The earliest must be the only one on its UTC calendar date: it is the baseline and adds no weight. The sizes of all later completions are divided by the whole UTC days from the baseline's date through the last completion's, both ends counted — Monday to Thursday is 4 days, which is the over N days in the header tooltip. Gaps between completions stay in that span; days since the last completion do not. Fewer completions than Minimum completions (default 4, at least 2, since the baseline alone gives no rate) or a tie on the earliest date shows no forecast.

Meetings and Todos: the meetings folder (root only), calendar filter and look-ahead; the todo folders, allowlisted section names, the assignee list and the fallback owner. Each tab appears once its folder is configured.

Chips#

Chips launch an agent (or any CLI) with a templated prompt, in the right repository. Two forms:

Virtual chips — recommended for recurring workflows. Defined once in settings as label | tool | repo | prompt, they appear on every card's right-click menu and in the note's file menu. Nothing to paste into notes, and a regenerated document can't lose them:

Refine              | claude | my-project | /refine {{id}}
Update ticket       | claude | my-project | /update-ticket {{id}}
Implementation plan | claude | my-project | /implementation-plan {{id}}

Variables: {{id}}, {{status}}, {{file}}, {{title}}. Column-header chips add {{ids}}, {{status}}, {{count}} for batch runs; meeting and calendar chips add {{date}} and {{title}}.

Block chips — for one-offs and generated reports. A fenced block anywhere in a note, carrying no commands and no paths — only a prompt, a tool name and a repo alias:

```dispatch
label: Refine this ticket
tool: claude
repo: my-project
prompt: |
  Refine {{file}}: read the spec, check open questions,
  and propose acceptance criteria.
```
  • prompt (required) — supports {{file}} (vault-relative path), {{title}} (basename), {{vault}} (vault path on this machine)
  • tool (optional) — defaults to the shared Default tool
  • repo (optional) — working-directory alias; defaults to the vault folder
  • label (optional) — button text

A chip refuses to launch when a variable it references is empty — a ticket with no id would otherwise send a bare /refine to an agent.

Tool commands#

Tools are defined per device as command templates:

claude = start "Dispatch" /d {{cwd}} cmd /k claude {{prompt}}
codex  = start "Dispatch" /d {{cwd}} cmd /k codex {{prompt}}

With more than one tool configured, clicking a chip offers one button per tool — Run with Claude, Run with Codex, Cancel — and the command preview follows whichever button is focused. With one tool the dialog is unchanged. The picker lives in the confirmation dialog only: with Confirm before running off, a chip runs its own tool (or the shared default) exactly as before.

Tool prompt prefix. Agents invoke a workflow with different characters — Claude /refine US42, Codex $refine US42. Set the character once per tool rather than a prompt per chip:

claude = /
codex  = $

A chip prompt that starts with / has that one character swapped for this tool. Prompts that are not commands — the column chips, which read "Work through these tickets…" — are never rewritten.

Tool prompts is the escape hatch for the case a prefix cannot express: a skill installed under a different name. One line per tool and chip, keyed tool.intent — give the chip an intent by suffixing its label in the chip-template row (Refine #refine), so the key survives renaming the button; without one the chip's label is the key:

codex.refine = $ticket-refine {{id}}

An explicit prompt wins over the prefix, and the chip's own prompt is used for anything neither names — so a device that configures nothing keeps working.

macOS:

claude = osascript -e 'tell app "Terminal" to do script "cd " & quoted form of {{cwd}} & " && claude " & quoted form of {{prompt}}'
codex  = osascript -e 'tell app "Terminal" to do script "cd " & quoted form of {{cwd}} & " && codex " & quoted form of {{prompt}}'

The two differ only in the binary: both agents take the prompt as one positional argument, and neither needs a flag. On macOS codex is on PATH from the installer, as it is on Windows — no hashed path is involved on either platform.

Variables: {{cwd}}, {{prompt}}, {{promptFile}} (the prompt written to a temp file — use it for long or multiline prompts). All expand as quoted arguments; append Raw for unquoted (there is deliberately no {{promptRaw}}).

Windows: avoid launching through wt.exe directly — Windows Terminal parses ; in its command line as a tab separator even inside quotes, so any prompt containing a semicolon breaks. start opens the user's default terminal (usually Windows Terminal anyway) without that parsing.

Run lifecycle#

When a chip launches a tool, Dispatch records the run in a machine-local file (~/.dispatch/runs/…jsonl) and passes DISPATCH_RUN_ID, DISPATCH_RUNS_FILE, DISPATCH_NOTE, DISPATCH_LABEL and DISPATCH_STARTED into the process.

Lifecycle hooks in the target repo — SessionStart/UserPromptSubmit/Stop/SessionEnd calling a small script — append records back. A ready-to-copy implementation ships with the setup plugin: plugins/dispatch-setup/skills/dispatch-setup/assets/run-state.mjs — drop it into the target repo as dispatch/scripts/run-state.mjs and wire the four events for each agent you run:

AgentWhere the hooks are wired
Claude Code.claude/settings.json, under hooks
Codex.codex/hooks.json, under a top-level hooks map keyed by event

One script serves both: it prefers the final message the agent hands it on the hook payload and otherwise reads that agent's transcript, so the run-log excerpt works either way.

The board then shows a live badge on the card: started → running ⇄ waiting → done, where waiting means the agent finished its turn and the session needs you. Done fades after 24 h; clicking a badge clears a ghost run. On completion the hook appends a run-log line to the note's ## Dispatch runs section, naming the agent that ran.

Codex only — hooks must be trusted, and trust is per entry. A correct .codex/hooks.json does nothing until you run codex interactively once in the repo and accept the prompt; until then every badge sits at started with nothing in the terminal to explain it. Trust is recorded per hook entry and hashed, so editing the file silently un-trusts what you changed — re-accept, then confirm a badge actually moves. A hooks file at the wrong path warns about nothing at all, and a malformed one only warns while the session runs on, so never take a clean start as proof the hooks are live.

The plugin only observes: live state stays on the machine running the agent, durable outcomes land in the note and sync with the vault.

One agent per working tree. Launching a chip into a repo that already has an active run offers Queue (starts when the blocking session ends), Run anyway, or cancel. The queue is in-memory; staleness caps (2 h launched, 24 h running) keep a killed terminal from blocking a repo forever. In a monorepo, define one alias per package if you want parallel sessions.

Automations#

Rules evaluated when a card enters a column (settings → Automations, JSON):

[
  { "when": ["Deployed"], "set": { "deployed": "{{date}}" }, "repo": "", "command": "" },
  { "when": [], "set": {},
    "repo": "my-project",
    "command": "node dispatch/scripts/move-ticket.mjs {{file}} {{from}} {{to}}" }
]
  • when — statuses that trigger the rule; empty = every status change.
  • set — frontmatter assignments written atomically with the status change ({{date}}, {{datetime}}, {{from}}, {{to}}). This is how deployed: gets stamped, which in turn feeds the release forecast.
  • command — optional shell command run in the repo alias, e.g. to mirror the move into your tracker. Variables: {{file}}, {{from}}, {{to}}, {{cwd}} (quoted; append Raw for unquoted). Commands are shared config but run only on devices that opt in (This device → Enable automation commands); set assignments always apply.

The config files on disk#

Everything in the settings tab is stored as JSON. The UI's compact forms — pipe-delimited column lines, tool = command lines — are input conveniences; on disk the shapes differ. This matters when an agent or a script writes the files directly. After editing either file outside Obsidian, click the board's ↻ reload button.

<vault>/.obsidian/plugins/dispatch/data.json — shared#

Missing keys fall back to the defaults in src/settings.ts, but writing the full object keeps the file readable and diffable:

{
  "board": {
    "sourceFolders": ["05_Requirements/Tickets"],
    "statusProperty": "status",
    "orderProperty": "rank",
    "columns": [
      { "value": "draft", "label": "Draft", "progress": 0 },
      { "value": "Ready for Refinement", "progress": 20 },
      { "value": "Refinement", "progress": 44, "wip": 5 },
      { "value": "Ready for Dev", "progress": 55 },
      { "value": "Development", "progress": 63, "wip": 4 },
      { "value": "Ready for Review", "progress": 86, "wip": 8 },
      { "value": "Deployed", "progress": 100 },
      { "value": "Rejected", "excluded": true }
    ],
    "titleProperty": "id",
    "assigneeProperty": "assignee",
    "badgeProperties": ["type", "priority", "version_target"],
    "questionsProperty": "open_questions",
    "testsProperty": "open_tests",
    "findingsProperty": "open_findings",
    "discussionProperty": "discussion",
    "requiredProperties": ["id", "status", "updated"],
    "automations": [
      { "when": ["Deployed"], "set": { "deployed": "{{date}}" }, "repo": "", "command": "" },
      { "when": [], "set": {}, "repo": "my-app", "command": "node dispatch/scripts/move-ticket.mjs {{file}} {{from}} {{to}}" }
    ]
  },
  "milestones": {
    "versionProperty": "version_target",
    "plannedVersions": ["v1.1.0", "v1.2.0", "v1.3.0"],
    "tags": { "1.2": "Beta" },
    "releaseOrderProperty": "release_rank",
    "sizeProperty": "size",
    "completedProperty": "deployed",
    "velocityWindowDays": 28,
    "velocityMinimumCompletions": 4,
    "releaseNotesFolder": "08_Delivery-and-QA/Releases"
  },
  "meetings": {
    "folder": "09_Meetings",
    "dateProperty": "meeting_date",
    "participantsProperty": "participants",
    "actionsProperty": "open_actions",
    "templates": [
      { "label": "Write meeting report", "intent": "meeting-report", "repo": "my-app", "prompt": "/meeting report {{title}}" }
    ],
    "calendarFilter": "",
    "calendarLookaheadDays": 14,
    "calendarChips": [
      { "label": "Prepare agenda", "intent": "meeting-agenda", "repo": "my-app", "prompt": "/meeting agenda {{date}} {{title}}" }
    ]
  },
  "todos": {
    "folders": ["09_Meetings", "05_Requirements/Tickets"],
    "sections": ["Action items", "Open action items"],
    "assignees": ["Alex", "Robin"],
    "fallbackAssignee": "Team"
  },
  "chips": {
    "defaultTool": "claude",
    "templates": [
      { "label": "Start refinement", "intent": "refine", "repo": "my-app", "prompt": "/refine {{id}}" },
      { "label": "Start development", "intent": "develop", "repo": "my-app", "prompt": "/develop {{id}}" }
    ],
    "columnTemplates": [
      { "label": "Refine all tickets", "repo": "my-app", "prompt": "Work through these tickets sequentially with the full /refine workflow: {{ids}}." }
    ]
  }
}

Where the stored shape differs from the settings UI:

  • Columns are objects, not value | Label | progress | WIP strings. label may be omitted (the value is then displayed), an omitted wip means no limit, and the UI's - progress becomes "excluded": true — not "progress": "-".
  • chips.templates and chips.columnTemplates are separate lists — card chips vs. batch chips on a column header. Both use label, repo and prompt; command chips should also have a stable intent, while tool is optional and should be omitted when the same chip must offer every configured agent. Only column prompts get {{ids}}, {{status}} and {{count}}.
  • Empty means off, and hides the tab. meetings.folder: "" hides the Meetings tab, todos.folders: [] hides Todos, milestones.completedProperty: "" turns the forecast off, board.orderProperty: "" disables manual ordering, milestones.releaseOrderProperty: "" keeps the Release Plan sorted by status, and an empty assigneeProperty/questionsProperty/testsProperty/findingsProperty/discussionProperty drops that badge.
  • The forecast numbers are read as whole numbers when the plugin loads. milestones.velocityWindowDays and milestones.velocityMinimumCompletions may be stored as numbers or numeric strings; fractions are rounded down. A value that is not a number or is below its floor (1 day, 2 completions) is replaced by the default (28, 4), which is what the settings tab then shows.
  • milestones.tags is keyed by normalized major.minor ("1.2": "Beta"), while plannedVersions only decides which columns exist, empty ones included. A drop on a version line writes that line's highest known patch — across its planned entries and its cards — in canonical vMAJOR.MINOR.PATCH form, a missing patch counting as .0: planned "v1.2.0" plus a card on 1.2.3 writes "v1.2.3". An expanded patch column writes its own patch ("v1.2.1"), a non-version label ("Icebox") is written as listed, and a card dropped back on the line it is already in is never rewritten. So the list is optional: a version a card already carries gets its column, and writes the same value, without it.
  • Automation rules always carry all four keys. A set-only rule keeps "repo": "" and "command": ""; an empty when means every status change.

~/.dispatch/<vault>-<hash>.json — this device#

{
  "repos": {
    "my-app": "C:\\Users\\me\\Workspace\\my-app"
  },
  "tools": {
    "claude": {
      "command": "start \"Dispatch Claude\" /d {{cwd}} cmd /k claude {{prompt}}",
      "promptPrefix": "/"
    },
    "codex": {
      "command": "start \"Dispatch Codex\" /d {{cwd}} cmd /k codex {{prompt}}",
      "promptPrefix": "$",
      "prompts": { "refine": "$ticket-refine {{id}}" }
    }
  },
  "calendarUrl": "",
  "enableHooks": false,
  "confirmBeforeRun": true
}
  • tools maps a name to an object, not to a string — {"claude": {"command": "…"}}. A bare string is not a valid tool entry. promptPrefix and prompts are optional in the schema; guided multi-agent setup writes an explicit prefix for each selected command-oriented agent so one neutral chip resolves correctly through every tool.
  • repos is the only place absolute paths may appear anywhere in Dispatch's configuration.
  • enableHooks gates automation commands on this machine; the set assignments of an automation rule always apply.

The filename is <vault name>-<hash>.json: the vault name with every run of non-[\w.-] characters replaced by _, and a djb2 hash of the vault's absolute path (backslashes included on Windows) as unsigned 32-bit hex:

let hash = 5381;
for (let i = 0; i < vaultPath.length; i++) hash = ((hash << 5) + hash + vaultPath.charCodeAt(i)) >>> 0;
const filename = `${vaultName}-${hash.toString(16)}.json`;

Settings → Dispatch → This device prints the resolved path — prefer reading it there; the derivation above is for headless setup. The runs file that lifecycle hooks append to sits beside it, under the same basename: ~/.dispatch/runs/<vault>-<hash>.jsonl.

The google block — optional Meet transcript import#

You almost certainly do not need this. The normal way to get a meeting transcript into your vault is to open the Gemini document in Google Docs and use File → Download → Markdown for both tabs, saving into your transcripts folder. /meeting report reads whatever is in that folder; it does not care how the file arrived, and nothing below is required for it to work.

What this section adds is skipping that download. It costs a Google Cloud project of your own, an OAuth consent screen on a domain you have verified in Search Console, and a published app — perhaps twenty minutes if you have done it before, and an afternoon if you have not. That is worth it if you run recurring meetings, or are setting Dispatch up for a team who should not each be exporting documents by hand. For one meeting a fortnight, download the file.

dispatch/scripts/meet-fetch.mjs imports a Google Meet meeting's Gemini document into the vault. It is a Dispatch-scope script — Dispatch ships it and it does the same thing for everyone — so its settings are ordinary device settings and live in the same per-vault file as everything else above, under a google key. (A script the project chooses, like a tracker sync, is configured by the project instead; that split is the whole of ADR-0027.)

It also ships in this repository rather than in the plugin bundle, so the import is available to people working from a clone. Installing Dispatch from the community directory does not put the script on your machine.

Paste the OAuth client JSON the Google Cloud Console gives you unchanged under google — the nested installed block is understood as-is. account is optional and only pre-selects the right identity on the consent screen; refresh_token is written by the script after consent, never by hand:

{
  "repos": { "my-project": "C:\\Users\\me\\Workspace\\my-project" },
  "calendarUrl": "https://calendar.google.com/calendar/ical/…/basic.ics",
  "google": {
    "installed": {
      "client_id": "….apps.googleusercontent.com",
      "client_secret": "…"
    },
    "account": "you@example.com"
  }
}

The script reads calendarUrl from the same file: the calendar feed carries each meeting's document as an ATTACH property, which is how it finds a transcript without any Drive permission at all.

Finding the file from a shell. The script must work with no Obsidian running, so it looks in three places: --config <path>, then the DISPATCH_LOCAL_SETTINGS environment variable Dispatch sets when it launches the script, then the single ~/.dispatch/<vault>-<hash>.json on the machine. With more than one vault it stops and asks for --config rather than guessing — Settings → Dispatch → This device prints the exact path.

⚠️ This file now holds a client secret and a refresh token. It always lived outside the vault and never syncs, but it used to contain only paths and command templates. Treat it as you would an SSH key, and note that anything copying it — including Dispatch's own "adopt settings" prompt when a vault moves — is copying credentials.

One-time setup in the Google Cloud Console, signed in as the account that holds the meetings:

  1. APIs & Services → Library — enable the Google Docs API. Nothing else; the Drive API is not used.

  2. Google Auth Platform → Branding — an app home page, privacy policy and terms of service, all on a domain you have verified in Search Console. Google rejects URLs on a domain you do not own, so a GitHub or plugin-directory page will not do.

  3. Data Access → Add or remove scopes — add one, pasting it into Manually add scopes if the picker does not list it:

    • https://www.googleapis.com/auth/documents.readonly — reads the meeting document. That is all the script needs: the calendar feed tells it which document, so nothing has to search your Drive.

    That is the entire list. Dispatch asks for no Google Drive access of any kind — a Drive scope is restricted, meaning an app verified on one needs an annual third-party security assessment, and the calendar feed makes it unnecessary.

  4. Audience → Publish app so the status is In production. In Testing, Google expires the refresh token after 7 days and you re-authorise every week. You do not need to submit for verification: an unverified production client works for its owner and for up to 100 consenting accounts, which is far more than a team. (Verification would be a consent-screen review rather than the annual third-party CASA assessment a Drive scope would need — Dispatch asks for no Drive scope.)

  5. Credentials → Create credentials → OAuth client ID → Desktop app. Desktop clients accept a loopback redirect with no registered URI, which is what the script uses.

Then, once per machine:

node dispatch/scripts/meet-fetch.mjs --auth

An "unverified app" screen is expected — Advanced → Go to … (unsafe). That is the consequence of step 4, not a fault. The refresh token is written back into the same device file, under google, and every later run is non-interactive. Re-run --auth if the script ever reports the token as no longer valid; Google revokes them on a password change.

Editing that file by hand while Obsidian is open is safe in both directions: the plugin re-reads the google block before saving its own settings, so it never writes over a token or a client you just put there.

Setting this up for a team. One person does the Console setup once; everyone else runs --auth against the same client and consents with their own Google account. Each teammate's refresh token stays on their own machine, in their own device file, and grants access only to documents they can already open. What is shared is the OAuth client, not the access. Anyone who would rather not is unaffected — they download the document and /meeting report reads it either way.

Security model#

Vault content is data, not code. Because notes sync across a team, Dispatch is built so that a note can never execute an arbitrary command:

  • Chip blocks only reference tools and repos by name; the actual commands and paths live in device-local settings.
  • Prompts are inserted as a single quoted argument (quotes and backslashes escaped, newlines flattened). For untrusted vaults, prefer {{promptFile}} in your tool templates.
  • Every chip click shows a confirmation dialog with the exact command by default; automation commands are off per device until enabled.

Caveat: commands run through your system shell. On Windows (cmd.exe), %VAR% sequences inside arguments are still expanded by the shell — another reason to keep the confirmation dialog on in shared vaults.

Disclosures#

  • Executes local processes — but only commands you configure on your device. Note content can never introduce a command; the confirmation dialog is on by default.
  • Reads/writes outside the vault — device settings at ~/.dispatch/<vault>-<hash>.json and run records at ~/.dispatch/runs/…jsonl, deliberately outside the vault so machine paths never sync.
  • One network request type — if (and only if) you configure a calendar ICS URL, the plugin fetches that feed read-only (cached 15 min) for the Meetings tab. Nothing else leaves your machine; no telemetry. Commands you configure act under your own credentials.
  • The plugin makes no Google requests. dispatch/scripts/meet-fetch.mjs does, when you run it — read-only, with credentials you supply. It ships in this repository, not in the plugin bundle; the plugin stores its settings and never uses them. See the privacy policy for what those scopes cover.

Building from source#

npm install
npm run dev     # watch build (main.js with inline sourcemap)
npm run build   # type-check + production build
npm test        # vitest suite against the fixture wiki in test/vault
npm run lint    # Obsidian's own plugin ruleset (what the directory review runs)

Symlink or copy the repo folder into a test vault's .obsidian/plugins/dispatch/, then use Obsidian's "Reload app without saving" command after a build.