You are viewing docs for v0.10.1 — see the latest release.
// docs/reference/files.md docs online

Files and directories

Vincent keeps two directories: config (things you edit) and data (things it owns). Both are platform-native, and both can be overridden.


Where they are

Purpose Linux macOS Windows
Config ~/.config/vincent/ ~/Library/Application Support/vincent/ %APPDATA%\vincent\
Data ~/.local/share/vincent/ ~/Library/Application Support/vincent/data/ %LOCALAPPDATA%\vincent\

On Linux, XDG_CONFIG_HOME and XDG_DATA_HOME are honored. macOS is the one platform where data nests inside config’s directory.

The config directory

{config_dir}/          # created 0700
  config.yaml          # daemon configuration — you or a client edit this, created 0600
  workflows/*.yaml     # global workflows, available to every project; 0644
  triggers/*.yaml      # event triggers, one {id}.yaml each; written 0600
  trigger-scripts/     # poll scripts for command triggers, by convention; 0700

The first three are watched. Editing config.yaml hot-reloads valid changes; saving a workflow file reloads the registry, and saving a trigger file reloads the triggers. None needs a restart.

triggers/ holds event triggers. It is global only: there is no .vincent/triggers/ in a repository. The daemon writes it on the /v1/triggers routes the TUI’s triggers view calls, and every write is 0600, new file or not, because a trigger’s argv can carry a secret. Like a workflow write, a trigger write carries a version token and a stale one is refused. A missing directory means no triggers. A directory the daemon cannot read keeps the triggers already loaded. A trigger’s cursor, poll status, delivery ledger and any events an overrun: queue mode is holding live in vincent.db, never beside the file. Deleting the file keeps its ledger and discards what it held, the same way it drops the cursor. vincent trigger apply writes it too, with the same 0600, when a create-trigger or update-triggers task installs its proposal.

trigger-scripts/ is where a command trigger’s poll script goes. vincent does not enforce the location or watch it; it is the convention the built-ins and the vincent-triggers skill follow, and a trigger names its script there by absolute path. It sits beside triggers/ rather than inside it, so no script shares a directory with the files the registry reads. On POSIX, keep the directory and each script 0700. A script reads credentials from the daemon’s environment rather than holding them, and it does not belong in a repository. See Event triggers.

config.yaml is also written by the daemon, on PATCH /v1/config — what vincent config set and the TUI’s daemon view call. It edits the key in place, so your comments and key order survive, and replaces the file atomically at 0600. A hand-edit racing a client edit is last-writer-wins.

workflows/ is written by the daemon too, on POST and PATCH /v1/workflows — what the TUI’s workflows screen calls when you create, fork or edit an entry. The directory is created when it does not exist yet, a new file is written 0644 rather than config’s 0600 because a workflow carries no secret and is meant to be shared, and an existing file keeps its mode. Edits are applied to the file’s own bytes, so your comments and key order survive. Unlike config.yaml, a workflow write is not last-writer-wins: a read hands back a version token and a write carrying a stale one is refused, so a hand-edit made since the client read the file cannot be silently overwritten. vincent workflow apply writes it too, with the same modes and the same version check, when a global update-workflows task installs the proposal you approved.

The directory and config.yaml are owner-only, because environment.set values are literal and people put tokens in them. On POSIX every daemon start drops group and other access from both — the same re-tightening the token file gets — and logs the path and the mode it found; vincent doctor reports the same as a warning with the exact chmod. That pass touches the modes only — it never rewrites your file’s contents — and if you widened those modes on purpose, the next start will narrow them again. On Windows the modes carry no access control and the per-user ACL of %APPDATA% applies instead.

Everything here is yours: it is safe to put under version control, sync between machines, or hand-edit — with one caveat from the modes above: a literal environment.set value travels with the file, so prefer inheriting the name. Nothing in this directory changes behind your back: a later start touches the modes above and no content, and the only other writes are the ones above, which happen when a client asks for them or a built-in’s task runs vincent trigger apply or vincent workflow apply.

Project-scoped workflows live in the repository instead, at .vincent/workflows/*.yaml, and shadow a global file of the same name.

The data directory

{data_dir}/                                         # created 0700
  vincent.db                                        # SQLite, WAL mode
  token                                             # API bearer token, 0600
  daemon.json                                       # { port, pid, started_at }
  daemon.lock                                       # single-instance lock
  tui.json                                          # TUI-local view state
  logs/daemon.log                                   # rotated, size-capped
  worktrees/{task_id}/                              # one git worktree per task
  worktrees/chat-{chat_id}/                         # one git worktree per chat (none for a chat opened on a task)
  transcripts/{task_id}/{step_index}-{attempt}.jsonl
  transcripts/chat-{chat_id}/{turn_seq}.jsonl
  trigger-proposals/{task_id}/                      # staged trigger files + manifest.json
  workflow-proposals/{task_id}/                     # staged global workflow files + manifest.json
  backups/vincent-backup-{timestamp}.tar.gz         # scheduled backups, when backup.dir is ""

On POSIX vincent creates {data_dir} owner-only (0700); on Windows the per-user ACL of %LOCALAPPDATA% applies instead. Vincent does not change the mode of a data directory that already exists.

File What it is
vincent.db Every project, task, step run and durable event. Only the daemon opens it — one writer, WAL mode, a single connection, which is what makes SQLITE_BUSY impossible in-process. Migrations are embedded and applied in a transaction at startup
token The API bearer token, created 0600 at first start. On Windows it relies on the per-user ACL of %LOCALAPPDATA%. Anyone who can read it can drive your daemon
daemon.json How clients find the daemon: port, pid, start time. Written atomically, removed on graceful shutdown
daemon.lock Single-instance enforcement; releases automatically when the process dies
tui.json TUI-local view state: the first-run full-auto acknowledgment, the board’s collapsed groups, and whether the offer to make vincent claude’s status line was declined. Written by the TUI, never read by the daemon; deleting it re-shows the full-auto notice, opens every group and lets the status-line offer come back
logs/daemon.log The daemon log, rotated and size-capped. Read by vincent daemon logs and the TUI’s daemon view — from disk in both cases, so it still works when the daemon is what died
trigger-proposals/{task_id}/ A trigger proposal a create-trigger or update-triggers task staged: the full proposed {id}.yaml files and manifest.json. The directory is 0700 and the files 0600, because a trigger’s argv can carry a token. vincent trigger apply removes it once every file is installed; a proposal that was rejected at its approval step stays until the task is deleted
workflow-proposals/{task_id}/ A proposal to change the global workflows, staged owner-only by an update-workflows task run with global: true: the whole proposed files, named as the live files are, and manifest.json. It sits outside {config_dir}/workflows, so nothing in it is live. vincent workflow apply removes it once every file is installed; a proposal that was rejected at its approval step stays until the task is deleted
backups/ Where scheduled backups go when backup.dir is "". Created 0700 the first time a scheduled backup runs, so it does not exist until you turn them on. Each archive is 0600

Worktrees and branches

Each task gets a git worktree at {data_dir}/worktrees/{task_id}/, checked out on a branch named:

vincent/{id}-{slug}

where slug comes from the task title. A title that sanitizes to nothing gives vincent/{id} with no trailing dash.

That is the default. The name is configurable — a template per project or in config.yaml, or a literal for one task — see Configuration.

A chat gets one on exactly the same terms, at {data_dir}/worktrees/chat-{chat_id}/ on its own vincent/{id}-{slug} branch. The directory is prefixed because tasks and chats share one root and are numbered independently — without it, chat 7 and task 7 would claim one path. The name is informational: vincent gc decides what is a stray from the database claims, which cover both tables, so a chat’s worktree is never mistaken for an orphan.

That is also why a task handed a chat’s worktree keeps living in worktrees/chat-{chat_id}/ for the rest of its life. The handoff transfers the claim, not the name, and nothing derives a task’s directory from its id — so the one directory it is safe to be surprised by is this one.

A chat opened on a stopped task is the exception to “a chat gets one”: it has no worktree directory of its own and no branch of its own. It works in the task’s worktree, on the task’s branch, and the task stays the only claim on both — so vincent gc sees nothing stray while the chat is open or after it closes, and closing it removes nothing. Its transcripts are still its own, under transcripts/chat-{chat_id}/.

Two rules worth internalizing:

  • The worktree is disposable. Archiving a task removes it. If it has uncommitted changes, archiving refuses unless forced — that work would be lost.
  • The branch is not — as long as it holds a commit. Vincent never deletes a branch that has commits past the base it was cut from, so everything an agent actually wrote accumulates in your repository until you remove it. The one branch archiving does delete is one that received no commit at all, which is what a workflow that never writes to the repository leaves behind (delete_empty_branch_on_archive). The daemon is the reliable list, since a configured name need not start with vincent/:

    vincent task ls --archived      # the branch column
    git worktree list
    

No task works in your own checkout unless you ask for one that does: vincent reads the repository to create worktrees, and the only change it makes there is fast-forwarding a base branch that is behind its remote, with its checkout when that is clean — see fetch_base_branch. A checkout with any change in it, and your stash, are never modified. A task created with --existing-branch on a branch that checkout holds is the exception: it runs there, with no worktree of its own and no ref moved.

What reclaims a worktree. Archiving the task, normally. A worktree whose task row is gone — a deleted project whose removal failed, a crash before the path was recorded — is nobody’s, and nothing archives it; that is what vincent gc is for. The daemon reports those at startup and counts them on the daemon view, but it never deletes one on its own.

Transcripts

{data_dir}/transcripts/{task_id}/{step_index}-{attempt}.jsonl
{data_dir}/transcripts/chat-{chat_id}/{turn_seq}.jsonl

One file per attempt — or, for a chat, per turn — JSONL. It contains the agent’s own event stream verbatim — lossless and replayable — interleaved with vincent’s namespaced vincent.* annotation lines.

Two consequences of storing the raw stream rather than a parsed one:

  • Normalization happens on read. Improving a parser improves transcripts already on disk: reasoning recorded in a run from last week renders today.
  • Unknown event types are kept. A line the parser does not recognize is transcripted anyway and surfaced in the TUI’s verbose output mode.

Transcripts are bounded by transcript_max_bytes per attempt and pruned for archived tasks past transcript_retention_days — see Configuration.

Both of those cover a chat too: a turn’s transcript is capped by transcript_max_bytes, and an ended chat’s directory under transcripts/chat-{chat_id}/ is reclaimed by the same retention pass, measured from when it ended — archived, handed off or closed, all three count, and a handed-off chat’s transcripts stay under chat-{chat_id}/, which the task that took its worktree never claims. A chat that has not ended keeps its transcripts however old they are, exactly as a task does.

What reclaims a transcript. Retention, for as long as the task row exists. Deleting a project deletes its task rows, and retention walks rows — so those directories are reached by no retention pass, ever, and are reclaimed by vincent gc instead.

vincent task delete and vincent chat delete are the third way. They remove an archived row and its transcript directory, in that order: the row goes first, so a failed unlink cannot resurrect something already reported gone — and vincent gc treats a transcript directory with no row as its own, which is what closes that gap. They are the only thing in vincent that deletes a task or a chat row; the retention pass removes files and never a row. Deleting a task also removes its trigger-proposals/{task_id}/ and workflow-proposals/{task_id}/ directories, under the same check that keeps the delete inside the data directory, and the closed chats opened on it along with their transcripts/chat-{chat_id}/ directories.

They contain everything the agent did. The rendered prompt or command an attempt was handed is recorded on the attempt’s own row in the database instead — the claude adapter passes the prompt on stdin, so no transcript ever carried it — and the task workspace’s Step Details tab and vincent task show <id> --step RUN are what read it back. Both hold whatever was substituted into them. Read one before pasting it into an issue.

Overriding the locations

VINCENT_CONFIG_DIR=/tmp/v-cfg VINCENT_DATA_DIR=/tmp/v-data vincent daemon start

Both override their directory outright. This is how the test suite isolates state, and it is the clean way to run a second throwaway instance beside your real one.

For a foreground daemon started by a manager with no per-process environment, the same values are available as flags:

vincent daemon --config-dir /srv/v-cfg --data-dir /srv/v-data

A service captures these at install time. A service does not inherit the shell that installed it, so vincent service install writes the directories in effect into the unit. If you change them, reinstall — otherwise your CLI and your service use different databases and each sees a world the other does not.

What is safe to delete

Path Deleting it means
logs/daemon.log Nothing; it is recreated
transcripts/{task_id}/, transcripts/chat-{chat_id}/ That task’s or chat’s output history is gone; the record and its metrics stay
trigger-proposals/{task_id}/ That task’s staged trigger proposal is gone, so its apply step has nothing to install
workflow-proposals/{task_id}/ That task’s staged global workflow proposal is gone, so its apply step has nothing to install
backups/vincent-backup-*.tar.gz That backup is gone. The next scheduled run is counted from the newest archive left, so deleting the newest one makes a run due sooner
worktrees/{task_id}/ Effectively an unregistered archive — prefer archiving the task, which does it properly. For a directory whose task no longer exists, prefer vincent gc, which checks it is not somebody’s live worktree first
daemon.json, daemon.lock Only safe while the daemon is stopped; both are recreated
token Recreated at next start, and every existing client must re-read it
vincent.db Everything is gone — projects, tasks, history. Branches in your repositories survive. This is the row Backup and restore exists for
{config_dir}/ Your config, global workflows, triggers and poll scripts; defaults are rewritten at next start

Backup and restore

vincent daemon backup ~/vincent-2026-08-25.tar.gz     # needs a running daemon
vincent daemon restore ~/vincent-2026-08-25.tar.gz    # needs a stopped one

One .tar.gz, four things in it:

Entry What it is
vincent.db A consistent copy of the database, taken with SQLite’s own VACUUM INTO
transcripts/ {data_dir}/transcripts/, in full
config/config.yaml, config/workflows/ Your config and global workflows
manifest.json The vincent version, the schema version, and when it was taken

Do not copy vincent.db by hand while the daemon runs. Under WAL a committed row lives in vincent.db-wal until a checkpoint, so a copy of vincent.db alone is missing recent work, and copying the three files separately gives a set that can restore into a torn database. The backup command exists because the obvious thing is the wrong thing. If you have no vincent binary to hand, the honest fallback is to stop the daemon first, then copy vincent.db, vincent.db-wal and vincent.db-shm together.

Backup needs a running daemon and refuses without one. Only the daemon opens the database, so only the daemon can copy it — the same rule vincent doctor --fix follows. It does not need a quiet daemon: VACUUM INTO writes elsewhere under a read transaction, so a backup may be taken while tasks are running.

The archive is as large as your history is. Transcripts are included in full and are not opt-out; the per-attempt cap alone is 512 MB (transcript_max_bytes). The command prints the bytes it wrote, broken down by database and transcripts, rather than pretending the artifact is small.

Not in the archive, and recoverable without it: worktrees/ (disposable — the branches they held are in your repositories), token, daemon.json, daemon.lock, logs/ and tui.json. A restored installation mints a fresh API token at next start, so every client re-reads it.

Restore is the only vincent command that touches the data directory directly, because the daemon it would overwrite has to be down for it to be safe. It refuses:

  • while a daemon is running — stop it first;
  • when the manifest’s schema version is newer than the binary you are running, since migrations are up-only and cannot be stepped back;
  • when the destination already holds vincent.db (or a leftover -wal/-shm), transcripts/, config.yaml or workflows/ — unless --force.

With --force each of those is moved aside as <name>.bak-<timestamp>. Nothing is deleted on any path, and the command prints where everything went.

Scheduled backups

The daemon can take the same archive on a timer. It is off until you set backup.interval:

vincent config set backup.interval 24h
  • Schedule. The interval is at least 1h, and it is counted from the newest scheduled archive already in the directory, so a restart does not reset it. A backup that is overdue, or a directory with none yet, runs as soon as the daemon starts. After that the daemon checks about once a minute, and a failed run is retried an hour later.
  • Where. backup.dir, which defaults to {data_dir}/backups/, created owner-only on first use. Archives are 0600 and named vincent-backup-<UTC timestamp>.tar.gz, for example vincent-backup-20260917T030000Z.tar.gz. An archive only gets that name once it is complete. A half-written one lives in a .vincent-backup-* directory beside it, which the daemon removes when it next starts.
  • Retention. After each successful run the daemon keeps the newest backup.keep scheduled archives (7 by default; 0 keeps everything) and deletes the rest. It only ever counts or deletes files with that name, so a vincent daemon backup archive you saved into the same directory, and anything else there, is never touched. A failed run deletes nothing.
  • Watching it. vincent doctor’s BACKUP group and the TUI daemon view show the last success, the next run, how many archives are kept and the last error. A failed last attempt is a doctor problem and makes vincent doctor exit 1. An overdue backup, for example after the machine was off, is not.

The default location is on the same disk as the database. A scheduled backup there protects against a corrupt database and against mistakes. It does not protect against losing the disk, and a disk that fills affects the database and the backups together. Point backup.dir at another disk for that protection.

A scheduled archive restores exactly like a manual one, with vincent daemon restore. Restore does not bring back backups/ itself: the archive holds the database, transcripts and config, not other archives.

To move an installation to another machine, restore into an empty pair of directories and start the daemon; your repositories and their branches are the other half, and vincent recreates worktrees as tasks need them.

One task back, without restoring everything. vincent task import <archive> <task-id> copies a single archived task — its row, its step attempts and its transcripts/{task_id}/ — out of an archive into the running daemon. It is the undo for vincent task delete. The task keeps its id, and its archived_at becomes the import time, so retention starts over rather than pruning what you just brought back. It refuses rather than replacing or merging: a task with that id, or a transcripts/{task_id}/ directory, already here is left alone.

To remove vincent entirely: vincent service uninstall, vincent daemon stop, delete the binary, delete both directories, then clean up any branches left in the repositories you used — the ones that carry commits, since the empty ones went when their tasks were archived. Take the list from vincent task ls --archived before deleting the database: a configured branch name need not start with vincent/.


See also