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.
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) isgetDataDir()=<CLAUDE_HOME>/agenthub, recomputed on every call. Changing CLAUDE_HOME at runtime moves every latergetDataDir()caller mid-process, and the next start opens an EMPTY database under the new home — indistinguishable from data loss. writeEnvFileappends 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
- A settings file outside both the repo and CLAUDE_HOME:
$XDG_CONFIG_HOME/agenthub/settings.env(default~/.config/agenthub/), overridable withAGENTHUB_SETTINGS_FILE(the server test runner points it at a temp path). Dir 0700, file 0600, atomic write, oneKEY=valueper line, no blank-line growth. New moduleserver/lib/settings-env.js. - Load order at startup (
server/index.js, replacingloadDotEnv): the real environment wins, then<repo>/.env, then the settings file (loadEnvFiles({owned: [.env], files: [settings]})). The checkout's.envranks above the per-user file because it is more specific: a dev checkout whose.envnames its ownDASHBOARD_DATA_DIRmust 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.envsupplied 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 inAGENTHUB_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 secondKEY=). - Pin the data dir on the first CLAUDE_HOME change. Before switching,
setClaudeHomewritesDASHBOARD_DATA_DIR=<the data dir in use>to the settings file and toprocess.envwhen 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. validateHomeDir(label, input)insettings-env.jsis 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.setCodexHome(server/lib/codex-home.js) andGET/PUT /api/settings/codex-home. PUT re-applies every AI Router profile a codex lane uses (reapplyCodexLayersinrouter-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-homegainenv_owned(additive).- UI: a shared
HomeDirSettingcomponent 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
.envlost to the plugin dashboard's pin. The settings file ranked above<repo>/.env, so aDASHBOARD_DATA_DIRpinned 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.envkey counts asenv_owned. - An old line could win.
writeSettingreplaced only the first line for a key; it now drops every earlier one,KEY = valueincluded, and writes through a symlinked settings file to its target. - A case-insensitive or bind-mounted path to the home passed.
validateHomeDirnow compares device and inode with the home and each ancestor. - Another site could set a home.
PUT /api/settings/claude-homeand/codex-homecarrysameOriginGuard(403EBADORIGIN): 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 answersrouters: {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.jsandscripts/import-history.jsread their CLAUDE_HOME-derived paths per call, not at load.cc-watcher.jsstill watches the startup home until a restart.