Migrating from a legacy install
If you’ve been running FlowForge as a per-project install (a .flowforge/ directory inside each customer repo), v1.0 moves your FlowForge state to the standalone architecture in one step — with a dry run, an automatic backup, and a rollback path if anything looks wrong.
This page is for existing FlowForge users. New users should start with the installation guide instead — there is nothing to migrate.
Before starting: you need to have completed flowforge install --global. The migration script writes to ~/.flowforge/repos/<repo-id>/ and that directory only exists after the global install. If you haven’t installed yet, start there.
Before you begin
Section titled “Before you begin”The migration tool is designed around three guardrails:
--dry-runis mandatory first. Every migration starts with a no-write dry run that reports what would happen.- Active timer = refuse. The migration script refuses to proceed while a session timer is running on the repo.
billing/time-tracking.jsonis the active write target during a session, and a concurrent write during the migration’smvcorrupts it unrecoverably. Stop the session first — your billing data is worth the 30 seconds. - Pre-migration backup is automatic. Before any move, a tarball of
.flowforge/is written to~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gz. This archive is not auto-deleted; it stays until you explicitly clean it up (a--finalizecommand is planned for v1.1, 90 days post-migration; for v1.0, the archive simply persists).
Step 1 — stop your session
Section titled “Step 1 — stop your session”flowforge tui# in the cockpit: end your session, OR/flowforge:session:pauseOr from the CLI:
flowforge version # confirm install# pause/end via your usual session commandIf you skip this step, the migration script catches it in pre-flight and refuses with a clear error message — you do not get to corrupt your billing data by accident.
Step 2 — dry run
Section titled “Step 2 — dry run”cd /path/to/your/projectflowforge migrate-from-legacy --dry-run--dry-run is mandatory before any live migration. The dry run:
- Computes the repo ID from your remote URL.
- Walks every entry in your
.flowforge/directory and reports its disposition: GLOBAL (already at~/.flowforge/), PER-REPO (will move under~/.flowforge/repos/<repo-id>/), SHIM (will become.git/hooks/shim +.claude/symlinks), DROP (test residue / stale backups, archived then removed), or REGEN (caches, regenerated on first use). - Confirms the timer-in-flight check passes.
- Exits with code 0 and no writes.
Read the dry-run output carefully. If you see anything you don’t expect — particularly under PER-REPO entries for billing/, audit/, or developers/ — stop and investigate before proceeding.
Step 3 — live migration
Section titled “Step 3 — live migration”flowforge migrate-from-legacyThe migration runs in this order:
- Pre-flight timer check. Refuses if a timer is running unless you pass
--force, in which case you also get an interactivey/Nprompt warning that billing data may be inconsistent.--forceis intentionally hard to use. Stop your session first — your billing data is worth the 30 seconds. - Idempotency guard. If the migration has already run for this repo (a
.migrated-from-legacymarker exists at~/.flowforge/repos/<repo-id>/), the script exits cleanly without re-running. - Pre-migration backup. Full tarball of
.flowforge/to~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gz. - Pre-create state dirs.
~/.flowforge/repos/<repo-id>/{billing,developers,sessions,logs,audit,...}are created. - Move PER-REPO state.
billing/,developers/<dev-id>/(preserving the per-developer partition exactly),sessions/,audit/, and the rest of the per-repo state move under~/.flowforge/repos/<repo-id>/.audit/is the highest-stakes move — the script verifies byte-equivalence after the move. - Drop test residue. Drift directories from older FF versions are merged into the canonical
developers/<dev-id>/partition or archived. Stale backups (e.g.,RULES.md.backup-20250821-173309) are archived. - Render hook shims and
.claude/symlinks at the structural floor. Five Git hook shims at<repo>/.git/hooks/;.claude/agentsand.claude/commandssymlinks;.claude/settings.jsonrendered. - Mark migrated with a
.migrated-from-legacyfile at the new state root, then rename the legacy directory:<repo>/.flowforge/→<repo>/.flowforge.legacy/. The legacy directory is preserved (renamed, not deleted) so a rollback is possible.
When the migration completes, your customer repo’s working tree contains:
.git/hooks/<5 shims>(new).claude/{agents, commands, settings.json}(new symlinks + rendered settings).flowforge.legacy/(your old state, renamed; preserved for rollback)
That’s it. No .flowforge/ directory anymore. Your billing history, your audit trail, your sessions, your tasks cache — all of that is now under ~/.flowforge/repos/<repo-id>/, byte-equivalent to where it was before.
Step 4 — verify
Section titled “Step 4 — verify”flowforge tuiShould land on the Repo Switcher with your repo listed. Press Enter to activate it, then check that:
- The header shows the correct project name.
- Your timer state is visible (idle, since you stopped your session in step 1).
- Your session history is intact (the workers panel and logs panel scope to your repo).
- The audit log queries work (
audit/was the highest-stakes move, and you want to confirm it’s queryable).
If anything is off, do not start a new session yet. Instead, see the rollback path below.
Rollback
Section titled “Rollback”flowforge migrate-from-legacy --rollbackThis reverses the migration:
- Removes the new state at
~/.flowforge/repos/<repo-id>/(your billing data is preserved in the pre-migration tarball under~/.flowforge/.legacy-archives/). - Renames
<repo>/.flowforge.legacy/back to<repo>/.flowforge/. - Removes the new hook shims and
.claude/shim from the customer repo.
The rollback path is byte-for-byte equivalent to the pre-migration state. If anything looks wrong after rollback, the pre-migration tarball at ~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gz is your final safety net.
After rolling back, you’re on the legacy .flowforge/ install. Your session timer, billing state, and audit log are intact. If you want to try the migration again, re-run from Step 1.
What gets cleaned up, and when
Section titled “What gets cleaned up, and when”The migration does not delete your old .flowforge/ directory — it renames it to .flowforge.legacy/. This is intentional:
The
.legacy/archive is NEVER deleted by migration; only by explicit--finalize.
For v1.0, .flowforge.legacy/ simply persists in your customer repo’s working tree. You can choose to delete it manually after a few weeks, once you’re confident the migration was successful — but FlowForge will not delete it for you. The --finalize cleanup command is on the v1.1 follow-up roadmap and is intended to run on a 90-day post-migration window.
If .flowforge.legacy/ is bloating your repo and you’re past the comfort window, a manual rm -rf .flowforge.legacy/ is safe — your real billing history and audit trail are at ~/.flowforge/repos/<repo-id>/, not in the legacy directory.
Is my billing data safe?
Section titled “Is my billing data safe?”Yes. Three independent safety nets:
- The pre-migration tarball at
~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gzis a complete snapshot of your.flowforge/directory captured before any moves. - The legacy directory is renamed (not deleted) to
.flowforge.legacy/, preserved in your customer repo until you choose to delete it. - The migrated state under
~/.flowforge/repos/<repo-id>/billing/is the live copy. The per-developer partition shape is preserved exactly, so the aggregator continues to work without changes.
The timer-in-flight gate (step 1 above) prevents the one scenario in which billing data could be corrupted: a concurrent write from task-time.sh during the migration’s mv. Stop your session, then migrate.
What about my audit trail?
Section titled “What about my audit trail?”The audit log at audit/ is the migration’s highest-stakes move. The script verifies byte-equivalence after moving audit/ from <repo>/.flowforge/audit/ to ~/.flowforge/repos/<repo-id>/audit/. If verification fails, the migration aborts and you can rollback.
The append-only JSONL format and the per-instance per-day rotation are unaffected by the directory move. Queries that worked before the migration work after it, against the new path.
Can I run side-by-side?
Section titled “Can I run side-by-side?”No. The v1.0 migration is a hard cutover; soft-cutover options would have provided dual-mode runtime windows but were not accepted.
The hard cutover is the right trade because we ran this migration on FlowForge’s own repo first — the safety net was tested against real billing data before this guide was written. The --dry-run, the pre-migration tarball, the .flowforge.legacy/ rename, and the --rollback path all combine to make the cutover safe; a soft-cutover would have added 90 days of dual-mode complexity for marginal additional safety.
What if I have multiple FlowForge repos?
Section titled “What if I have multiple FlowForge repos?”Migrate them one at a time. Each repo’s migration is independent — different repo IDs, different state directories under ~/.flowforge/repos/<repo-id>/. There’s no global lock, no cross-repo coupling. Stop the session on the repo you’re about to migrate, run --dry-run, run the live migration, verify, then move to the next repo.
If you have many repos and want to script the migration, you can. The dry run and live migration are both idempotent; running the migration on an already-migrated repo exits cleanly via the idempotency guard.
What if ~/.flowforge/repos/<repo-id>/ already exists from a previous attempt?
Section titled “What if ~/.flowforge/repos/<repo-id>/ already exists from a previous attempt?”The idempotency guard at step 2 catches this. If the migration completed previously, it exits cleanly without re-running. If you need to start over (e.g., after a --rollback), the rollback removes the new state directory, so a subsequent flowforge migrate-from-legacy will run from scratch.
My CI broke after migrating — what do I do?
Section titled “My CI broke after migrating — what do I do?”CI runners typically don’t have FlowForge installed, and the v1.0 hook shims fail-open in that case — the shim hits the [[ ! -x "$FF_BIN" ]] branch and exits 0, allowing CI’s own validators to run unimpeded.
If your CI has FF_HOOK_FAIL_CLOSED=1 set in its environment (e.g., inherited from a ~/.zshrc or ~/.bashrc on a self-hosted runner), the shims fail-closed instead. This is a known footgun — the env var is meant for one-shot per-invocation overrides, not persistent shell-rc inheritance. For persistent fail-closed configuration on machines that should fail-closed, use ~/.flowforge/config.json instead:
{ "hooks": { "fail_closed": true }}Do not set FF_HOOK_FAIL_CLOSED=1 in shell rc files.
References
Section titled “References”- ADR-0025: Single-Install Architecture — the canonical decision and the four-risk list (Risk #1 governs migration).
- STATE-MIGRATION-MAP.md — the state inventory with dispositions, plus the migration script outline and the timer-in-flight gate.
- HOOK-SHIM-CONTRACT.md — the post-migration hook router contract, including fail-open semantics and
FF_HOOK_FAIL_CLOSEDscope rules. - ADR-0023: Audit Trail Policy — the moat-substrate policy that constrains how
audit/is moved.