1
Write Safety
Latte edited this page 2026-06-29 13:48:58 +00:00

Write Safety

familiar began "read-only by design" and is now read + write — but writing is gated behind a single, deliberately strict safety layer. This page explains exactly when a write happens and what stops it.

The three conditions

A write only happens when all of these hold:

  1. The tool is called with confirm=true (otherwise it returns a dry-run preview and changes nothing).
  2. Writes are enabled globally (on by default; opt out with familiar writes disable).
  3. The action runs through writes.guard — the single choke point every mutating tool must use.

Every executed write is appended to an audit log on disk.

The gate: writes.guard

writes.py exposes one function that every write tool calls:

async def guard(action, params, confirm, execute):
    # confirm=False            -> dry-run preview, execute nothing
    # confirm=True, writes off  -> raise WriteDisabled
    # confirm=True, writes on   -> execute(), append to audit log, return result

confirm=false → dry-run preview

The default. Nothing is changed. The tool returns:

{
  "dry_run": true,
  "action": "set_waypoint",
  "params": { "...": "..." },
  "note": "Nothing was changed. Pass confirm=true to execute (writes must be enabled)."
}

This is how the AI can show you what it would do before you commit. You confirm by asking it to run the same action with confirm=true.

confirm=true but writes disabled → WriteDisabled

If you've globally turned writes off, a confirmed write still does nothing and raises WriteDisabled telling you to run familiar writes enable to re-allow it.

confirm=true and writes enabled → execute + audit

The action runs, and a line is appended to the audit log (see below).

The global toggle

Writes are on by default. The toggle is controlled three ways, in precedence order:

  1. Environment variable FAMILIAR_ENABLE_WRITES — if set, it wins (1/true/yes/on = enabled).
  2. On-disk flag file writes_disabled in the data dir — created/removed by the CLI.
  3. Default — enabled (no flag file present).

CLI:

uv run familiar writes            # show current state
uv run familiar writes disable    # create the flag → block all writes
uv run familiar writes enable     # remove the flag → re-allow writes

The write_status MCP tool reports the current state to the AI without touching the toggle.

The audit log

Each executed write appends one JSON line to audit.log in the data dir:

{"ts": 1719600000.0, "action": "send_mail", "params": { "...": "..." }}

Dry-runs and blocked (disabled) writes are not logged — only real, executed writes are. The log lives alongside the encrypted token store (see Configuration for the path).

Which tools are writes?

Every tool marked WRITE in the Tool Reference:

  • Comms: send_mail, set_mail_read, respond_calendar_event
  • Contacts: add_contacts, remove_contacts
  • Fittings: create_fitting, delete_fitting
  • Fleet: invite_to_fleet, update_fleet
  • UI: set_waypoint, open_window

These are powered by the OAuth write scopes granted at login (see Authentication & Scopes).

For contributors

When adding a write tool:

  • Give it a required character and a confirm: bool = False parameter.
  • Route the mutation through writes.guard — never call a mutating ESI endpoint directly.
  • Pass a clear action string and the user-facing params so the dry-run preview and audit line are meaningful.