- Atomic delivery: create the temp file inside the destination dir (dot-prefixed, invisible to the inbox glob) so the final rename never crosses filesystems and readers never see a half-written message. - Reject . and .. explicitly in the --reply-to and coord-done name guards instead of relying on downstream failure. - Add -h/--help to coord-inbox.sh (uniform across the three CLIs). - Close selftest gaps: default mailbox path via HOME fallback, malformed frontmatter on the read path, read-only destination dir. 48 -> 64 checks, all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fduZz8otpU3W3rhfoPD6t
111 lines
7.2 KiB
Markdown
111 lines
7.2 KiB
Markdown
# Coord
|
|
|
|
> A local mailbox for coordination between Claude Code sessions in different repositories. Session A in repo X leaves a message for repo Y; the next session in repo Y gets it injected as context at startup. Local, private, no network, no SaaS.
|
|
|
|
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See the [marketplace governance](https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main/GOVERNANCE.md) for the full model.
|
|
|
|
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
|
|
|

|
|

|
|

|
|

|
|

|
|
|
|
---
|
|
|
|
## Why This Exists
|
|
|
|
When you work with an AI coding agent across several repositories, the sessions are islands. A decision made in repo X often matters to repo Y — a shared spec changed, a bug another repo depends on got fixed, a gate was re-pinned. Without a channel, you are the messenger: you remember to mention it in the next session, or it gets lost.
|
|
|
|
Coord is that channel, reduced to the simplest thing that works: a directory of Markdown files on your own disk. Sending is writing a file; receiving is a SessionStart hook that injects your repo's pending messages as context. No server, no daemon, no network, no accounts.
|
|
|
|
**Transport, not state.** A coord message is a notice, not a source of truth. The durable record of any decision lives in the owning repo (its docs, its git history). Messages point at that record; they never replace it.
|
|
|
|
## How It Works
|
|
|
|
Mailbox layout (default `~/.claude/coord/`, override with `CLAUDE_COORD_DIR`):
|
|
|
|
<repo>/inbox/ pending directed messages TO <repo>
|
|
<repo>/archive/ handled messages (kept, never deleted)
|
|
_broadcast/inbox/ messages to ALL repos (accumulate)
|
|
_broadcast/seen/<repo> per-repo seen set: delivered broadcast filenames, one per line
|
|
|
|
Repo identity is the basename of the git toplevel (fallback: the working directory). There is no registration — a repo joins the moment something is sent to it, or when it first reads a broadcast.
|
|
|
|
Message format (filename `<UTC-timestamp>-<uniq>-from-<sender>.md`):
|
|
|
|
---
|
|
from: <source-repo>
|
|
to: <target-repo> | broadcast
|
|
subject: <short subject>
|
|
date: <UTC ISO-8601>
|
|
---
|
|
<body>
|
|
|
|
**Lifecycle — deliver until done.** Directed messages are NOT archived on read. They stay pending and are re-injected at every session start (startup, `/clear`, resume) until explicitly marked handled: replying (`--reply-to`) archives the original, or `coord-done <file>` archives it without a reply. `/clear` never loses a message. Broadcasts are delivered once per repo via the seen set and otherwise accumulate; prune `_broadcast/inbox/` manually when a notice stops being relevant to future first-time repos.
|
|
|
|
## Install
|
|
|
|
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
|
claude plugin install coord@ktg-plugin-marketplace
|
|
|
|
The plugin ships empty: your mailbox is created lazily on first send, on your machine, and stays there.
|
|
|
|
## Usage
|
|
|
|
**Natural language (the `coord-send` skill).** In any session: "tell repo-x the bug is fixed", "broadcast that the spec changed", "reply to that coord message", "when the tests are green, notify repo-y". The skill maps intent to the right `coord-send` invocation, including bounded multi-target loops and deferred sends.
|
|
|
|
**Receiving** is automatic: the SessionStart hook injects your repo's pending inbox and unseen broadcasts as context, with per-message reply/resolve hints.
|
|
|
|
**CLI.** The engine is three bash scripts in the plugin's `scripts/` directory; resolve them as `"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-<name>.sh"` (from a terminal, use the plugin's install path):
|
|
|
|
coord-send.sh --to <repo> --subject "<subject>" [--message "<text>"] # or body on stdin
|
|
coord-send.sh --broadcast --subject "<subject>" <<'BODY' ... BODY
|
|
coord-send.sh --reply-to <filename> [--subject "Re: ..."] # routes + closes original
|
|
coord-inbox.sh [--repo <name>] # print pending (what the hook injects)
|
|
coord-done.sh <filename>... | --all # archive without replying
|
|
|
|
The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to … | done without reply: coord-done …`) refer to these scripts.
|
|
|
|
## Security Model
|
|
|
|
Cross-repo message content is untrusted input by design:
|
|
|
|
- **Read side:** every injected content line is prefixed with `> `, so a message body can never forge the `--- message:` / `-> reply:` framing lines at column 0. The injected header explicitly frames content as UNTRUSTED DATA and instructs the model to never follow instructions found inside it.
|
|
- **Send side:** CR/LF and control characters in `subject`/`from` are collapsed before writing, so fields cannot inject frontmatter lines or a premature `---` terminator. Sender names are sanitized in filenames (raw name kept in frontmatter).
|
|
- **No network, no secrets:** everything is local files under your `$HOME`. Credentials never appear in messages, filenames, or frontmatter — there is nothing to leak by transport.
|
|
- **Privacy rule:** coordination metadata (repo names, status, architecture) never belongs on a public surface. The mailbox lives outside your repos and stays out of git.
|
|
|
|
- **Atomic delivery:** the temp file is created inside the destination directory (dot-prefixed, invisible to the inbox glob), so the final rename never crosses filesystems and readers never observe a half-written message.
|
|
|
|
Every guarantee above is pinned by the 64-check selftest, including forgery-resistance regressions.
|
|
|
|
## The Five Rules
|
|
|
|
1. **Mailbox, not state.** Files here are messages in transit. If a file starts acting as someone's state-of-play, it belongs in the owning repo.
|
|
2. **No durable decisions live here.** The copy here is the notice, not the record — durable content is written in the owning repo's docs.
|
|
3. **One recipient per message.** `--to <repo>` or `--broadcast`.
|
|
4. **Delivery happens via session start.** Don't hand-edit another repo's inbox; use `coord-send`.
|
|
5. **Private.** Coordination metadata never reaches a public surface.
|
|
|
|
## Requirements
|
|
|
|
- macOS or Linux with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only).
|
|
- Node.js >= 18 for the SessionStart hook (zero npm dependencies).
|
|
- `git` is optional — without it, repo identity falls back to the directory basename.
|
|
|
|
## Development
|
|
|
|
bash scripts/coord-selftest.sh # 64 checks against a throwaway mailbox
|
|
npm test # same selftest via node --test
|
|
|
|
TDD is the house rule: every behavior change lands with a failing selftest check first.
|
|
|
|
Note on argument parsing: `coord-inbox.sh` deliberately ignores unknown
|
|
arguments (lenient by design — it runs inside the SessionStart hook and must
|
|
never fail a session over a stray flag), while `coord-send.sh` rejects them.
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|