# Design decisions

Use this log to understand constraints that should survive refactors and proposals that
remain open. Runtime contracts live in [Protocol](PROTOCOL.md); this page records why the
boundaries exist.

## Preserve these decisions

| Decision | Reason |
|---|---|
| Local-first, same machine | Coordination that needs a server is coordination nobody sets up. |
| Git optional | The problem is concurrent sessions, not version control; a plain directory works. |
| Attach everywhere, materialise lazily | Universal attachment is what makes it ambient; lazy state is what makes a lone session free. |
| No coordinator, workstream, or task subsystem | ACC connects peers; it does not assign, supervise, schedule, or own their work. |
| Explicit peer conversations are first-class product data | Addressed questions, answers, decisions, and handoffs need durable threads and per-recipient receipts. |
| Intent is awareness, not assignment | A session says what it is doing without creating a queue or granting another session authority. |
| Claims are workspace-global | Independently opened sessions still share a filesystem. |
| Runtime state outside the repository | A checkout can be deleted, cloned, or synced; presence and locks must not travel with it. |
| Project config optional and runtime-free | A committed file is editable by anyone with a PR, so it may carry policy — never sessions or tokens. |
| Durable state is authoritative | Next-turn or live delivery may accelerate an interaction; neither becomes the source of truth. |
| Bounded discovery, explicit history recovery | Automatic context stays small; peers select exact messages or historical records when they need full bodies. |
| Immutable explicit decision links | Replacements and withdrawals preserve prior records and expose competing branches without choosing by timestamp. |
| Fence managed runtime activation | Refresh all integrations before switching the active generation; partial refresh stays fenced until forward recovery completes. |
| No heartbeat helper in v1 | An idle session is honestly reported `stale`. A sidecar process to fake liveness is worse than the truth. |
| Presence reads the process, never writes the record | A pid answers "gone" at once; an age floor covers what a pid cannot, including its own reuse. Nothing is written back — no session has authority to edit another's record. This checks a process already there, not one beating on a session's behalf like the heartbeat helper above. |
| No AI client or session launching | ACC attaches to sessions you already own; its separate update worker manages ACC packages, not those sessions. |
| One publishable package | One version and one release rather than twelve coordinated ones. |
| MIT | Widest reuse, least friction. |
| Node 24 or newer required | Uses `node:test`, modern `fs` promises, and no transpiler. |
| MCP session from launch config | Never from `initialize` or `clientInfo` — those are attacker-controllable. See the threat scenarios in [SECURITY_MODEL.md](SECURITY_MODEL.md) (scenario 8). |

## Avoid these rejected designs

| Rejected | Why |
|---|---|
| A file in each repo that agents poll | No guard, no identity, no atomicity — and it ends up committed. |
| A permanent global lead session | Turns peers into workers and makes one crash fatal. |
| Treating MCP as an external-client lifecycle guarantee | Tool calls resolve MCP's own participant, but cannot infer the client's lifecycle, intercept writes, or wake its receiver. |
| Requiring Git, tmux, PostgreSQL, or a service | Every requirement is a reason the tool is not installed. |
| Collecting or sharing raw transcripts | Raw transcripts are never collected or shared. Explicit peer messages are bounded records the sender chose to address. |
| Reporting queued messages as delivered | A delivery state that overstates itself is worse than no state. |
| Guessing file paths out of shell commands | It would block work at random and still miss real writes. See [CAPABILITIES.md](CAPABILITIES.md). |

### Reversals backed by failures

**Reversed in 0.1.11: removing the client's hook-trust record on uninstall.**
0.1.9 took Codex's `[hooks.state."<plugin>:…"]` tables out on uninstall,
reasoning they named a plugin that was gone. The check behind that reasoning
perturbed the record instead of removing it — a hook whose recorded hash no
longer matched was still observed running, so absence was never actually
tested.

Absence was sufficient to disable the hooks in that historical capture. With no
record, that client ran no hook, printed `hook: SessionStart Completed` while
executing nothing, and ACC's write guard went silently off — while `acc doctor`
and `codex plugin list` both reported it enabled. A shell write walked through a
guarded claim; writing the exact same hashes back (captured before deletion,
from an ACC three releases older) revived the guard immediately.

That is why removal is never ACC's to do: it preserves the client's trust decision.
The historical observation above does not establish trust across arbitrary upgrades.
Codex 0.153.4 compares each current definition's hash and reports changed definitions
as modified; a trusted hook can also be disabled. ACC preserves the records but leaves
current readiness unverified, directing the user to `/hooks` in Codex.

**The generalisation, since this cost a release:** to learn whether state is
load-bearing, take it away. Changing it tests something else.

**Reversed in 0.1.7: reading write positions out of shell commands.** The
rejected row above still holds against *guessing* — it stays because its
reasoning shaped the replacement. What changed was the evidence: a live Codex
session asked to append to a file another agent held guarded went through
untouched via `printf ... >> file`. Agents here are told to prefer the shell
for edits, so this was the common path, not an edge case.

The fix is to read, not guess: only a redirection, or the operand of a
command whose job is to put bytes somewhere, counts as a write position. A
read is never reported, so the failure the rejected row feared — blocking
work at random — cannot occur; only paths a command would actually write get
declared. Coverage is deliberately partial, and
[CAPABILITIES.md](CAPABILITIES.md) says where it ends: an agent that knows
where a guard stops behaves better than one that believes it absolute.

## Decide these later

1. **Storage backend.** The hardened filesystem store ships first. A transactional backend
   can go behind the same interface later — but not merely to avoid a dependency.
2. **Remote coordination.** v2, or a separate product.
3. **AI client/session launching.** Possibly never; possibly an external integration.
4. **Multi-root discovery rules** beyond the current `roots` list.
5. **Default claim lease length** for hook-only adapters. A hook-only session cannot sustain
   a short renewal cadence, so lease policy must not assume one.

---

See also: [README](index.md) for navigation and [Glossary](GLOSSARY.md) for terms.
