# Self-hosted departure checklist

Source: https://codeherder.com/docs/self-host-departure/

Retire a self-hosted install in order. Each step names a command or route and its expected result. A blank rehearsal record follows.

Use this checklist when your organization leaves CodeHerder, or retires an install. Your operator runs it. Rehearse it once on a test install first. Your support agreement says whether the support contact takes part.

Run [Self-hosted exit and data preservation](https://codeherder.com/docs/self-host-exit/) first. Do not start step I1 until every export passes its completeness check. A step marked **irreversible** cannot be undone.

Record each step in the [rehearsal record](https://codeherder.com/docs/self-host-departure/#rehearsal-record). A step is done when you see its expected result.

## Identity

| # | Step | Command or route | Expected result |
| --- | --- | --- | --- |
| I1 | List the people | `ch member list` | Every human in the workspace. |
| I2 | Deprovision each human | `POST /v1/workspaces/{id}/members/{memberId}/deprovision`, from the member action in the web app. With SCIM, deactivate the user in your identity provider. | The member is disabled. Its roles, API keys, MCP grants, CLI installations and device links are revoked. See [Offboard a person](https://codeherder.com/docs/self-hosting/#offboard-a-person). |
| I3 | Revoke every credential of each human (belt and braces) | `ch human revoke-all <human> --yes`. Instance operator only. | The reply shows the revoked counts and `boundSeconds`. Every server process refuses the credentials within that bound. |
| I4 | Revoke the SCIM token | `DELETE /v1/sso-connections/{id}/scim-token` | Your identity provider’s next SCIM call fails with 401. |
| I5 | Revoke each agent key | `ch member keys list <agent>`, then `ch member keys delete <hash>` for each key. Then `ch agent disable <agent>`. | The key list shows no active key. The agent is paused. |
| I6 | Disable the Cognito app client, then the pool | AWS console, or `aws cognito-idp delete-user-pool-client`. To delete the pool, first `aws cognito-idp update-user-pool --deletion-protection INACTIVE`, then `aws cognito-idp delete-user-pool`. **Irreversible.** | A sign-in at the hosted UI fails. See [Self-hosted Cognito sign-in](https://codeherder.com/docs/self-host-cognito/). |

The Cognito pool is not part of the database. A database restore does not change it.

## Devices

| # | Step | Command or route | Expected result |
| --- | --- | --- | --- |
| D1 | List the devices | `ch device list` | Every linked device. |
| D2 | Unlink each device from each workspace | `ch device workspaces delete <device> --workspace <workspace>` | The device drops the workspace from its served set. On its next reaper cycle it removes the repo mirrors, pool directories, shared knowledge folder and transcripts for that workspace. |
| D3 | Revoke each device token | `ch device revoke-token <device> <tokenId>` | The tunnel closes. A reconnect with that token gets 401. |
| D4 | Archive each device | `ch device archive <device>` | The device leaves every list and placement. All its tokens are revoked. |

Do the next steps on each device host.

| # | Step | Command | Expected result |
| --- | --- | --- | --- |
| D5 | Stop the device server, then delete the session root | Stop the process that runs `ch device-server`. Then delete `$CH_SESSION_ROOT` (default `~/codeherder-sessions`). | `ls` finds no session root. |
| D6 | Sign out, then delete the local roots | `ch logout`, then delete `~/.codeherder` (or `$CH_CONFIG_DIR`) and the credential store root if you moved it with `CH_CLAUDE_CREDENTIAL_STORE`. | `ls` finds no directory. |
| D7 | Delete harness transcripts kept by Docker stages | Delete `~/.claude/projects/`. | `ls` finds no directory. |
| D8 | Remove the tool-cache volumes | `docker volume ls`, then `docker volume rm <name>` for each `/opt/ch-tools` volume. | `docker volume ls` shows none. |

`ch logout` revokes the CLI credential on the server and removes the cached credential. It does not delete the local store, the shared-document cache or the local tasks. Step D6 deletes them. This is a known gap.

### What a device keeps

Every row below is a store the device server or `ch` writes. The last column names the step that removes it.

| Store | Path | Removed by |
| --- | --- | --- |
| `Session worktree and sandbox` | `<root>/<sandboxId>/ (checkouts, session files)` | Step D5: delete the session root. |
| `Per-session knowledge copy` | `<root>/<sandboxId>/<leaf>/knowledge/` | Step D5: delete the session root. |
| `Agent CLI config redirect` | `<root>/<sandboxId>/<session area>/isolated-config/ (aws, docker, kube, gh, glab)` | Step D5: delete the session root. |
| `Repo mirrors` | `<root>/.mirrors/<workspaceId>-<repoHash>/ (bare clones)` | Step D5: delete the session root. |
| `Warm worktree pool` | `<root>/.pool/<workspaceId>-<repoHash>/` | Step D5: delete the session root. |
| `Shared knowledge folder` | `<root>/.knowledge/<workspaceId>/, with .staging/ and .retired/ beside it` | Step D5: delete the session root. |
| `Transcript archive` | `<root>/.transcripts/<anchorId>/<harness>/` | Step D5: delete the session root. |
| `Reclaimed work bundles` | `<root>/.reclaimed/` | Step D5: delete the session root. |
| `Harness config area` | `under the sandbox directory: the relocated harness config dir (transcripts, harness settings)` | Step D5: delete the session root. |
| `Provision parameters` | `<root>/.provision-params/<sessionId>.json (repo URLs, branches)` | Step D5: delete the session root. |
| `Spawn cwd pins` | `<root>/.spawn-cwd/<sessionId>.json` | Step D5: delete the session root. |
| `Foreign-path attestations` | `<root>/.foreign-attest/<sandboxId>.json` | Step D5: delete the session root. |
| `Foreign-path anchors` | `<root>/.foreign-anchor/<sandboxId>.json` | Step D5: delete the session root. |
| `Git common-dir anchors` | `<root>/.git-common-dir-anchors/<sandboxId>/` | Step D5: delete the session root. |
| `Agent, git-credential and device-local sockets` | `the agent socket dir, <runtime dir>/git-credential/ and the device-local bridge dir (unix sockets, no data at rest)` | Stopping the device server removes the sockets. Nothing to delete. |
| `Empty git hooks directory` | `~/.codeherder/empty-git-hooks/ (empty; core.hooksPath target)` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Session state files` | `~/.codeherder/sessions/*.json (PTY holder state)` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Execution receipts` | `~/.codeherder/sessions/receipts/ and receipts/refused/` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Egress denial outbox` | `~/.codeherder/sessions/egress-outbox/<runId>/<reportId>.json (destination host, port, transport, denial category, counts)` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Docker stage-run files` | `~/.codeherder/stage-runs/<runId>/ (env file, bearer dir, pi auth, transcript mount)` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Host harness transcripts` | `~/.claude/projects/<cwd slug>/ (Docker stages; unrelocated fallback)` | Step D7: delete `~/.claude/projects/`. |
| `Tool-cache Docker volume` | `Docker volume mounted at /opt/ch-tools, one per task worktree` | Step D8: `docker volume rm`. |
| `Credential store and pool` | `<credential store>/credentials/<harness>.env and <harness>.d/ (pool entries)` | Step D6: delete the credential store root. Then revoke the credential at its provider (I14). |
| `Local launch directories` | `~/.codeherder/launches/<launchId>/ (status, registration, runtime log)` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Device-server log` | `~/.codeherder/logs/device-server.log (+ .1) and last-trim.json` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `ch local store` | `~/.codeherder/local.db (+ backups); sync outbox, bases, cursors` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Local attachment blobs` | `~/.codeherder/blobs/` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Shared-document cache` | `<CLI config home>/shareddocs/<endpoint>/<workspaceId>/collection.json` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `CLI endpoint credentials` | `<CLI config home>/endpoints/<endpoint>/ (CLI tokens)` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Cost markers` | `~/.codeherder/cost-markers/` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |
| `Held cost evidence` | `~/.codeherder/cost-held/<transcriptHash>/<turnIndex>.json` | Before departure, resolve pending usage evidence. Step D6 then deletes this root, including expired and quarantined entries; normal workspace unlink and sandbox teardown retain it. |
| `Stage-tools template` | `~/.codeherder/stage-tools/ (injected stage config and gitconfig)` | Step D6: delete `~/.codeherder` (or `CH_CONFIG_DIR`). |

## Integrations

| # | Step | Command or route | Expected result |
| --- | --- | --- | --- |
| I7 | Delete each integration | `GET /v1/workspaces/{id}/integrations`, then `DELETE /v1/integrations/{id}` for GitHub, GitLab, Slack and PagerDuty. In the web app: Settings → Integrations. | The list is empty. |
| I8 | Revoke the token or uninstall the app at the provider | The provider’s console. | The provider shows no CodeHerder token or app. |
| I9 | Delete each outbound webhook | `ch webhook list`, then `ch webhook delete <id>` | `ch webhook list` shows none. |
| I10 | Delete each inbound endpoint | `ch inbound-endpoint list`, then `ch inbound-endpoint delete <ref>` | The list is empty. A sender’s next POST gets a 404. |
| I11 | Delete each device git token | On each device host: `ch device git-token list`, then `ch device git-token delete <label>` | The pool is empty. |
| I12 | Delete each device credential pool entry | On each device host: `ch device credential list`, then `ch device credential delete <label>` | The pool is empty. |
| I13 | Delete variables | `ch variable list`, then `ch variable unset <KEY>` at each scope | `ch variable list` shows none. |
| I14 | Revoke each AI key at its provider | The Anthropic console, Bedrock IAM, or your gateway. | A call with the old key fails. |

## Backups

| # | Step | Command or route | Expected result |
| --- | --- | --- | --- |
| B1 | State the retention | Read your snapshot and dump retention. | A written number of days for the database, the bucket and the revocation journal. |
| B2 | Keep the revocation journal | Keep `revocation-journal/` or `CH_REVOCATION_JOURNAL_DIR` at least as long as the longest backup. | The journal is older than the oldest backup. |
| B3 | Restore a pre-departure snapshot into a scratch environment | See [Recover](https://codeherder.com/docs/self-host-backups/#recover). Start the server fenced. | The server starts and refuses to send email or to accept devices. |
| B4 | Try a revoked API key, and read a revoked device token’s row | `curl -H "Authorization: Bearer <key>" <addr>/v1/workspaces`. Then, in the restored database: `SELECT revoked_at FROM device_tokens WHERE id = '<tokenId>';` | Before replay, the key may work and `revoked_at` may be NULL. Note both results. |
| B5 | Replay the revocations | `ch instance restore-fence replay --yes` | The command succeeds. |
| B6 | Check that no entry is left | `ch instance restore-fence show` | No pending journal entry. |
| B7 | Try the same key again, and read the same token row | Repeat B4. | The key gets 401. `revoked_at` shows a time. |
| B8 | Promote | `ch instance restore-fence promote --yes` | The server leaves the fence. Never promote before B7 passes. |

Erased and revoked data stays in each snapshot until that snapshot expires. See [Erased data in backups](https://codeherder.com/docs/self-host-backups/#erased-data-in-backups). The restore fence replays revocations and erasures. It does not delete a Cognito identity. Delete it by hand if you also rolled the pool back.

A device token does not sign in to a REST route, so B4 reads its row instead. A fenced server refuses every device tunnel, so a tunnel test cannot run before B8. After B8, you can try the revoked token on a device. Expect the server to refuse the tunnel.

CodeHerder has no automated test that a restored database never revives access. Use B3 to B7 as your proof until that test exists.

## Retire the workspaces

Do this only after every export passed. **Irreversible.**

1. Archive: `ch workspace archive <workspace>`. Expect the workspace to leave every list.
2. Delete: `ch workspace delete <workspace>`. Only an owner can run it. Expect the workspace to be gone.

## Rehearsal record

Your operator and a witness fill this table during a rehearsal on a test install. Leave a row blank until the step runs. Evidence is the command output, a screenshot, or a ticket id.

| Area | Step | Date | Who ran it | Evidence | Result (pass, fail) |
| --- | --- | --- | --- | --- | --- |
| Exit | Export and completeness check |  |  |  |  |
| Identity | I1 to I3 |  |  |  |  |
| Identity | I4 to I6 |  |  |  |  |
| Devices | D1 to D4 |  |  |  |  |
| Devices | D5 to D8 |  |  |  |  |
| Integrations | I7 to I10 |  |  |  |  |
| Integrations | I11 to I14 |  |  |  |  |
| Backups | B1 to B2 |  |  |  |  |
| Backups | B3 to B8 |  |  |  |  |
| Retire | Archive and delete |  |  |  |  |

Signed off by (operator): ______________________ Date: ____________

Witnessed by: ______________________ Date: ____________
