Files
AgentHub/docs/superpowers/specs/2026-09-29-settings-persistence-design.md
nntrivi2001 969b06253c feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from
Settings → Dashboard to the Claude Code tab's AgentHub sub-tab
(ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub.
A home that is or contains the OS home is refused (EBADHOME).

Settings persistence: dashboard-saved settings go to
~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin
cache an update wipes. Load order: real env > the checkout's .env > that file.
The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put.

C1: codex-usage.js reads Codex rollout journals into sessions (new column
tool) and token_usage, tracked per file in codex_rollouts; forks deduped by
total vector, sub-agent threads folded into the root; pricing-sync also
prices OpenAI models; PricingSettings with a vendor filter.

C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on
a click, through a stable forwarder; Codex trust is never written. The hook
route creates tool='codex' sessions and folds sub-agent hooks into the root.

D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens
from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js
installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never
PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab;
Gemini pricing.

Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing
on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=,
clear-data covering the new tables, Codex child-thread events moving no
status, hooks owning status only while current, EFOREIGN for a forwarder
that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE
honoured from .env.

Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
2026-09-29 16:09:19 +07:00

6.3 KiB

Settings persistence + editable CODEX_HOME

Issue: #40 follow-up to part B. Date: 2026-09-29. Status: self-reviewed (user asked for autonomous work with self-review per part).

Problem

setClaudeHome persists through writeEnvFile, which writes <repo>/.env. For a plugin install that directory is the plugin CACHE, replaced on every update, so a saved CLAUDE_HOME silently reverts. Two more faults ride along:

  • The data dir (database, runtime, ai-tools.json) is getDataDir() = <CLAUDE_HOME>/agenthub, recomputed on every call. Changing CLAUDE_HOME at runtime moves every later getDataDir() caller mid-process, and the next start opens an EMPTY database under the new home — indistinguishable from data loss.
  • writeEnvFile appends a blank line on every save.

An editable CODEX_HOME (deferred from part B) has the same persistence problem, plus one of its own: the AI Router layer <name>.config.toml lives in $CODEX_HOME, so after a change a codex lane on a router would start with -p <name> against a home that has no such layer.

Design

  1. A settings file outside both the repo and CLAUDE_HOME: $XDG_CONFIG_HOME/agenthub/settings.env (default ~/.config/agenthub/), overridable with AGENTHUB_SETTINGS_FILE (the server test runner points it at a temp path). Dir 0700, file 0600, atomic write, one KEY=value per line, no blank-line growth. New module server/lib/settings-env.js.
  2. Load order at startup (server/index.js, replacing loadDotEnv): the real environment wins, then <repo>/.env, then the settings file (loadEnvFiles({owned: [.env], files: [settings]})). The checkout's .env ranks above the per-user file because it is more specific: a dev checkout whose .env names its own DASHBOARD_DATA_DIR must not be pointed at the live database by a pin saved from the plugin dashboard. A key is only set when still unset, exactly as before. The keys the real environment or .env supplied are remembered (envOwned), so a route can say a saved value will be overridden at the next start. Keys that came from a file are named in AGENTHUB_FILE_KEYS: a self-restart spawns the successor with this process's env, and without the marker the successor would count them as the real environment's and warn falsely. A saved path may not contain a line break (it would inject a second KEY=).
  3. Pin the data dir on the first CLAUDE_HOME change. Before switching, setClaudeHome writes DASHBOARD_DATA_DIR=<the data dir in use> to the settings file and to process.env when neither already sets it. The database and runtime dir stay where the plugin bootstrap and the CLI (which never read the settings file) look for them. The discovery file (<CLAUDE_HOME>/.agenthub.json) is NOT pinned and follows CLAUDE_HOME, as it did before this change; hooks under a different CLAUDE_HOME then fall back to port 4820.
  4. validateHomeDir(label, input) in settings-env.js is the one path check both homes use: ~/ expansion, absolute, path.resolve, exists, is a directory, and neither IS nor CONTAINS the OS home (EBADHOME) — /, /home, ~ itself, or a symlink, bind mount or differently-cased path to any of them, compared by device and inode against the home and each of its ancestors.
  5. setCodexHome (server/lib/codex-home.js) and GET/PUT /api/settings/codex-home. PUT re-applies every AI Router profile a codex lane uses (reapplyCodexLayers in router-apply.js), so the layer exists in the new home, and answers { ok, codex_home, env_owned, routers: { updated, failed } }. GET/PUT /api/settings/claude-home gain env_owned (additive).
  6. UI: a shared HomeDirSetting component renders CLAUDE_HOME (Claude Code → AgentHub) and CODEX_HOME (new Codex → AgentHub sub-tab). It shows the env-owned warning, the router re-apply failures, and — for CODEX_HOME — that a running codex pane keeps its old home until restarted.

Not changed

Existing <repo>/.env files keep working, and rank above the settings file: a key a .env sets is env_owned, so a value saved from the UI for it is overridden at the next start, and the UI says so. The file is per OS user, so every AgentHub server of that user (a plugin install and a dev checkout) reads the same one. Stores that live under CLAUDE_HOME by design (agenthub-routers/, agenthub-profiles/, agenthub-git-credentials/) still follow it. The CLI and bootstrap do not read the settings file; pinning the data dir is what keeps them consistent.

Tests

Server: settings-env parse/load order/envOwned/write (no blank lines, 0600), validateHomeDir refusals, setClaudeHome pinning DASHBOARD_DATA_DIR, setCodexHome + the route (refusal 400, success, router re-apply called), OpenAPI entry. Client: HomeDirSetting save/error/env warning, the Codex sub-tab. Browser check on an isolated server.

Found in the whole-change review, fixed

  • A dev checkout's .env lost to the plugin dashboard's pin. The settings file ranked above <repo>/.env, so a DASHBOARD_DATA_DIR pinned from the plugin dashboard pointed a dev checkout's server at the live database. The order is now real environment > .env > settings file, and a .env key counts as env_owned.
  • An old line could win. writeSetting replaced only the first line for a key; it now drops every earlier one, KEY = value included, and writes through a symlinked settings file to its target.
  • A case-insensitive or bind-mounted path to the home passed. validateHomeDir now compares device and inode with the home and each ancestor.
  • Another site could set a home. PUT /api/settings/claude-home and /codex-home carry sameOriginGuard (403 EBADORIGIN): a home decides which config a pane runs.
  • Lanes silently lost their router. The router store is <CLAUDE_HOME>/agenthub-routers/, so a new home may lack a lane's profile; the claude-home PUT answers routers: {missing: [...]} (lanesWithMissingRouter) and the UI warns that those lanes start on native.
  • Stale paths after a change. The Claude hook status (settings.js), scripts/install-hooks.js and scripts/import-history.js read their CLAUDE_HOME-derived paths per call, not at load. cc-watcher.js still watches the startup home until a restart.