Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to tokenmaxxing are documented here. Versions are anchored t

## Unreleased

### Fixed

- Carried custom `CLAUDE_CONFIG_DIR` and `CODEX_HOME` locations into scheduled syncs.

## 0.6.0 - 2026-08-05

### Added
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ tokenmaxxing logout # Revoke this device's CLI token

The background service supports macOS, Linux, and Windows. It uses the global
`tokenmaxxing` binary and keeps itself current through the package manager that
installed the CLI when that package manager can be detected.
installed the CLI when that package manager can be detected. Custom Claude
(`CLAUDE_CONFIG_DIR`) and Codex (`CODEX_HOME`) log roots are captured at
`tokenmaxxing service install` or `tokenmaxxing service repair`; rerun one of
those after changing them.

## Privacy

Expand Down
4 changes: 3 additions & 1 deletion apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ You can also install globally with `bun add -g --trust @851-labs/tokenmaxxing@la
The background service uses the global `tokenmaxxing` binary and syncs every
5 minutes. It auto-updates through the package manager that installed the
global binary (bun, npm, pnpm, or yarn) when that package manager can be
detected.
detected. Custom Claude (`CLAUDE_CONFIG_DIR`) and Codex (`CODEX_HOME`) log
roots are captured at `tokenmaxxing service install` or
`tokenmaxxing service repair`; rerun one of those after changing them.
Use `tokenmaxxing service status` for the last run and `tokenmaxxing service
doctor` to inspect scheduler files, auth, auto-update, locks, and recent logs.

Expand Down
97 changes: 96 additions & 1 deletion apps/cli/src/commands/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { chmod, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, dirname, join } from "node:path";
import { promisify } from "node:util";
import { gzipSync } from "node:zlib";

const execFileAsync = promisify(execFile);

import { Cause, Effect, Layer } from "effect";
import type { AuthUser } from "@tokenmaxxing/api-contract";
import { describe, expect, it } from "vitest";
Expand Down Expand Up @@ -481,9 +485,44 @@ describe("servicePaths", () => {
});
});

describe("capturedServiceEnv", () => {
it("captures nonempty Claude and Codex log roots and omits empty values", () => {
expect(
capturedServiceEnv({
CLAUDE_CONFIG_DIR: "/synthetic/Claude Logs, extra",
CODEX_HOME: "/synthetic/Codex Logs",
HERMES_HOME: "/data/hermes",
HOME: "/home/alex",
PATH: "/usr/bin",
TOKENMAXXING_API_TOKEN: "tmx_secret",
}),
).toEqual({
CLAUDE_CONFIG_DIR: "/synthetic/Claude Logs, extra",
CODEX_HOME: "/synthetic/Codex Logs",
HERMES_HOME: "/data/hermes",
HOME: "/home/alex",
PATH: "/usr/bin",
});

expect(
capturedServiceEnv({
CLAUDE_CONFIG_DIR: "",
CODEX_HOME: undefined,
HOME: "/home/alex",
PATH: "/usr/bin",
}),
).toEqual({
HOME: "/home/alex",
PATH: "/usr/bin",
});
});
});

describe("renderServiceWrapper", () => {
it("runs sync with a durable command without embedding package-manager updates", () => {
const env = capturedServiceEnv({
CLAUDE_CONFIG_DIR: "/synthetic/Claude Logs, extra",
CODEX_HOME: "/synthetic/Codex Logs",
HERMES_HOME: "/data/hermes",
HOME: "/home/alex",
PATH: "/usr/local/bin:/usr/bin",
Expand All @@ -503,6 +542,8 @@ describe("renderServiceWrapper", () => {
expect(wrapper).toContain("[ ! -r '/home/alex/.config/tokenmaxxing/service-runner-current' ]");
expect(wrapper).toContain('"$runner" service run --scheduled');
expect(wrapper).toContain("export HERMES_HOME='/data/hermes'");
expect(wrapper).toContain("export CLAUDE_CONFIG_DIR='/synthetic/Claude Logs, extra'");
expect(wrapper).toContain("export CODEX_HOME='/synthetic/Codex Logs'");
expect(wrapper).not.toContain("bun update");
expect(wrapper).not.toContain("npm install");
expect(wrapper).not.toContain("pnpm add");
Expand Down Expand Up @@ -544,8 +585,13 @@ describe("renderServiceWrapper", () => {
});

it("renders Windows wrappers without package-manager updates", () => {
const env = capturedServiceEnv({
CLAUDE_CONFIG_DIR: "C:\\Users\\alex\\Claude Logs",
CODEX_HOME: "C:\\Users\\alex\\Codex Logs",
PATH: "C:\\Windows\\System32",
});
const wrapper = renderServiceWrapper({
env: { PATH: "/usr/bin" },
env,
logPath: "/tmp/tokenmaxxing.log",
platform: "win32",
runnerPointerPath: "C:\\Users\\alex\\AppData\\Roaming\\tokenmaxxing\\service-runner-current",
Expand All @@ -561,6 +607,55 @@ describe("renderServiceWrapper", () => {
expect(wrapper).toContain('move /y "%TOKENMAXXING_LOG%" "%TOKENMAXXING_LOG%.1"');
expect(wrapper).toContain("set /p TOKENMAXXING_SERVICE_RUNNER=<");
expect(wrapper).toContain("service run --scheduled");
expect(wrapper).toContain('set "CLAUDE_CONFIG_DIR=C:\\Users\\alex\\Claude Logs"');
expect(wrapper).toContain('set "CODEX_HOME=C:\\Users\\alex\\Codex Logs"');
});

it("passes captured Claude and Codex roots through a generated POSIX wrapper", async () => {
const root = await mkdtemp(join(tmpdir(), "tokenmaxxing-service-env-"));
try {
const runnerPath = join(root, "fake-runner");
const pointerPath = join(root, "service-runner-current");
const logPath = join(root, "service.log");
const claudeRoot = join(root, "Claude Logs, extra");
const codexRoot = join(root, "Codex Logs");

await writeFile(
runnerPath,
`#!/bin/sh
printf 'CLAUDE_CONFIG_DIR=%s\\n' "$CLAUDE_CONFIG_DIR"
printf 'CODEX_HOME=%s\\n' "$CODEX_HOME"
`,
{ encoding: "utf8", mode: 0o755 },
);
await chmod(runnerPath, 0o755);
await writeFile(pointerPath, `${runnerPath}\n`, "utf8");

const wrapperPath = join(root, "tokenmaxxing.sh");
await writeFile(
wrapperPath,
renderServiceWrapper({
env: capturedServiceEnv({
CLAUDE_CONFIG_DIR: claudeRoot,
CODEX_HOME: codexRoot,
HOME: root,
PATH: "/usr/bin:/bin",
}),
logPath,
platform: "linux",
runnerPointerPath: pointerPath,
}),
{ encoding: "utf8", mode: 0o755 },
);
await chmod(wrapperPath, 0o755);

await execFileAsync("/bin/sh", [wrapperPath], { timeout: 5000 });
const log = await readFile(logPath, "utf8");
expect(log).toContain(`CLAUDE_CONFIG_DIR=${claudeRoot}`);
expect(log).toContain(`CODEX_HOME=${codexRoot}`);
} finally {
await rm(root, { force: true, recursive: true });
}
});
});

Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/commands/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4205,6 +4205,8 @@ function capturedServiceEnv(
"LOCALAPPDATA",
"APPDATA",
"HERMES_HOME",
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"TOKENMAXXING_CONFIG_DIR",
"TOKENMAXXING_ENV",
"TOKENMAXXING_API_URL",
Expand Down