Skip to content

Add a --purge argument #17

Description

@ableinc

Only when the --purge argument is provided should the data directories we cleared. The --purge argument should be paired with the uninstall argument. If it is not, then uninstall should only remove the service and not the data.

Activity

  1. ableinc commented on Aug 29, 2026

    @ableinc
    OwnerAuthor

    Plan

    Gate --uninstall's data deletion behind a new --purge flag

    Context

    Today --uninstall (internal/install.Uninstall, called from cmd/agent.go:115) always destroys the operator's state: it removes workspace.root, workspace.repos_root, workspace.logs_root, store.path, and claude.usage_cache_path (removeConfiguredStatePaths, internal/install/install.go:431), and — if --install created the dedicated coding-agent-loop system account — runs userdel -r, deleting that account's entire home. There is no way to remove the systemd service while keeping run history, transcripts, cloned repos, and SQLite state.

    Issue #17 asks for a --purge flag that gates exactly that deletion. Plain --uninstall should remove only the service (unit file, /opt/coding-agent-loop, systemd registration); data directories should only be cleared when --purge is also passed. --purge is only meaningful alongside --uninstall.

    Approach

    1. cmd/agent.go — the flag

    • Add purge bool to the flags struct (after uninstall).
    • Register it: flag.BoolVar(&f.purge, "purge", false, "with --uninstall: also delete the service's data — workspace.root, workspace.repos_root, workspace.logs_root, store.path, claude.usage_cache_path, and the dedicated service account's home. Without it, --uninstall removes only the service and leaves all data in place").
    • Reject --purge on its own: add a small validateFlags(f) error called from main immediately after flag.Parse() (before the --print-service short-circuit, so every invocation is checked uniformly) returning --purge only applies to --uninstall (try: sudo coding-agent-loop --uninstall --purge) when f.purge && !f.uninstall. Exit non-zero via the existing fmt.Fprintf(os.Stderr, ...) / os.Exit(1) pattern.
    • Pass it through: install.Uninstall(install.UninstallOptions{ConfigPath: ..., Purge: f.purge, Log: ...}).
    • Update usageHeader (cmd/agent.go:48): the --uninstall line says it removes the service only, plus a new sudo coding-agent-loop --uninstall --purge also delete workspace/logs/state data line.

    2. internal/install/install.go — gate the destructive steps

    • Add Purge bool to UninstallOptions, documented as: state paths and the dedicated account's home survive when false.

    • Rewrite Uninstall's doc comment to separate unconditional steps (stop/disable, remove unitPath, daemon-reload, remove installRoot) from purge-only steps. Keep the existing note that claude.credentials_path is never touched under any flag.

    • Replace the direct removeConfiguredStatePaths(t.home, opts.ConfigPath, log) call (install.go:312) with a purge-aware helper. Keep loadStatePaths, expandHome, removeDir, removeFile exactly as they are and restructure only the caller:

      // resolvedStatePaths returns the directories and files the service was
      // configured to write, resolved against home.
      func resolvedStatePaths(home, fallbackConfigPath string, log func(string, ...any)) (dirs, files []string)
      
      // applyStatePaths removes those paths when purge is set, and otherwise logs
      // exactly what was left behind and how to remove it.
      func applyStatePaths(home, fallbackConfigPath string, purge bool, log func(string, ...any))

      applyStatePaths replaces removeConfiguredStatePaths. Its non-purge branch still resolves the paths (so the operator is told where their data lives) and logs one line per retained path plus re-run with --uninstall --purge to delete this data. Resolution must still happen before installRoot is removed, since installedConfigPath lives there — keep the call in its current position in Uninstall.

    • Gate the dedicated-user block (install.go:337-350) on opts.Purge. When not purging, keep the user.Lookup(dedicatedUser) check but only log that the account and /home/coding-agent-loop were left in place, naming --uninstall --purge as the way to remove them. Recommendation: do not run userdel at all when not purging — userdel without -r orphans the home to a dangling uid, and a later --install recreates the account with a possibly different uid, leaving the retained data unreadable. Keeping account and home together is the only variant where "keep the data" actually holds.

    • Everything else stays unconditional: systemctl disable --now, removing unitPath, daemon-reload, os.RemoveAll(installRoot).

    3. Makefile

    • Add purge to .PHONY (line 17).
    • Reword the ## uninstall: help comment to "stop, disable, and remove the service; leaves data in place". Keep the existing build-if-missing shim and its comment verbatim (it exists because uninstalling must work without a valid config.json for go:embed).
    • Add a purge target using the same shim, invoking sudo $(BINARY) --uninstall --purge, with a ## purge: help line marking it destructive. Either factor the shim into an internal ensure-binary prerequisite shared by both targets, or repeat the four-line block — either is acceptable.

    4. README.md

    • CLI flags table (~line 152): change the --uninstall row to "stop, disable, and remove the systemd unit and /opt/coding-agent-loop; leaves all data in place", and add a --purge row: "only with --uninstall: also delete the configured workspace/logs/state paths and the dedicated service account's home".
    • "To remove everything --install created" section (~lines 845-871): show both commands (--uninstall, --uninstall --purge, make uninstall, make purge) and split the numbered list into "always" steps (today's 1, 3, 4) and "only with --purge" steps (today's 2 and 5), keeping the existing prose about non-default state paths and about running --uninstall the same way --install was run.
    • Quick start (~line 118) needs no change.

    5. Tests — internal/install/install_test.go

    Existing coverage (TestUninstallRequiresRoot, removeDir/removeFile/loadStatePaths tests) stays. Add tests against the new applyStatePaths seam, which needs no root:

    • TestApplyStatePathsRemovesWhenPurging — temp home with .agent-loop/work, .../logs, .../state.db; point installedConfigPath at a temp config via the existing setInstalledConfigPathForTest helper; purge=true; assert the paths are gone.
    • TestApplyStatePathsKeepsDataWhenNotPurging — same fixture, purge=false; assert every path still exists and that the log callback fired.
    • TestApplyStatePathsHonoursConfiguredPaths — config naming non-default absolute paths under a temp dir, purge=true; assert those exact paths are removed, locking in that purge follows the config rather than assuming ~/.agent-loop.

    Risks / decisions for the reviewer

    • Behaviour change for existing users. Anyone relying on make uninstall / --uninstall to wipe state now needs --purge. That is the explicit intent of the issue; the README and usage-text changes are what make it discoverable.
    • Dedicated-user handling (main judgement call). This plan leaves the coding-agent-loop account and its home in place when not purging. The alternative — delete the account, keep the home — is rejected because it strands data behind an orphaned uid. Worth a reviewer nod, since it means plain --uninstall does not fully reverse --install in that path.
    • /opt/coding-agent-loop/config.json is still deleted by plain --uninstall, since it goes with os.RemoveAll(installRoot) as today. I read the issue's "data directories" as the state paths, not the installed config copy. If the reviewer wants the config preserved on a non-purge uninstall, the change would be to remove installRoot/bin and the unit while leaving config.json behind (note installConfig already refuses to clobber an existing config on re-install).
    • --purge alone errors rather than warning — the issue calls it a paired flag, and a silent no-op on a destructive-sounding flag is worse than a clear failure.

    Verification

    go build ./... && go vet ./...
    go test -race ./internal/install/...
    make test                                  # full suite
    bin/coding-agent-loop --purge              # expect exit 1 with the pairing error
    bin/coding-agent-loop --help               # usage shows --purge and the reworded --uninstall

    Manual end-to-end (throwaway VM/container with systemd; these steps need root):

    1. make install, let the service create ~/.agent-loop/{work,repos,logs} and state.db.
    2. sudo bin/coding-agent-loop --uninstall → unit gone, /opt/coding-agent-loop gone, ~/.agent-loop and its contents intact, log lines naming each retained path.
    3. make install again, then sudo bin/coding-agent-loop --uninstall --purge → same service removal plus ~/.agent-loop gone; confirm ~/.claude/.credentials.json still exists.
    4. In the no-SUDO_USER path (run as root in a container): non-purge leaves the coding-agent-loop account and /home/coding-agent-loop; purge removes both.

    Reply with exactly implement to approve this plan and start the change. Reply with anything else and the plan will be revised to address it.

    coding-agent-loop run fb571323-4ec1-4435-bd4c-961adace0b22, model claude-opus-5, cost $0.9100

  2. ableinc commented on Aug 30, 2026

    @ableinc
    OwnerAuthor

    implement

  3. ableinc commented on Aug 30, 2026

    @ableinc
    OwnerAuthor

    Opened a draft pull request for this issue: #20

    Tests failed (make test) — see the PR for output.

    Comment implement again if you want another attempt at this issue.

    coding-agent-loop run 2f5f1598-467b-4d0f-a207-27131c31f3e2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions