Skip to content
Merged
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This file is the project's committed home for project-intrinsic agent knowledge:
- `--json` emits the normalized model, and `--full` is required before account identity or per-source attempts are shown.
- JSON provider reports include `provider`, `label`, `source`, `windows`, and `state`; `state.retryAfter` can appear for provider rate limits, and `state.reason: keychain_access_required` plus `state.remedyCommand` can appear when a stale or unavailable Claude result is blocked by a skipped macOS Keychain prompt.
- macOS Claude Keychain value reads are skipped on plain calls until a successful value read records the non-secret access marker under the quota-axi cache directory; after that, plain calls may reuse the existing grant and read live Claude quota.
- Managed-profile, Claude identity, and Codex executable-override contracts are documented in [README Security Posture](README.md#security-posture).
- `--allow-keychain-prompt` is the first-time opt-in that permits the Claude Keychain value read which can prompt, and agents should relay the one-time "Always Allow" grant when `keychain_access_required` advice appears.
- Codex uses `$CODEX_HOME/auth.json` or `~/.codex/auth.json` OAuth before the CLI fallback.
- Codex `auth.json` support is OAuth-token only; never treat `OPENAI_API_KEY` as valid quota auth or send API keys to ChatGPT quota endpoints.
Expand Down
40 changes: 22 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ $ quota-axi --provider claude --json
"state": {
"status": "fresh",
"stale": false,
"sourcesTried": ["oauth"],
"sourcesTried": ["oauth", "oauth-profile"],
"refreshedAt": "2026-03-15T16:41:55.000Z"
}
}
Expand All @@ -111,7 +111,7 @@ auth[7]{provider,source,path,status,error}:
claude,oauth-file,~/.claude/.credentials.json,available,none
claude,keychain,none,skipped,keychain_prompt_required
codex,auth-json,~/.codex/auth.json,available,none
codex,cli-rpc,none,available,none
codex,cli-rpc,~/.local/bin/codex,available,none
cursor,state-vscdb,~/Library/Application Support/Cursor/User/globalStorage/state.vscdb,available,none
copilot,apps-json,~/.config/github-copilot/apps.json,available,none
grok,auth-json,~/.grok/auth.json,available,none
Expand Down Expand Up @@ -224,8 +224,10 @@ It is generated from `src/skill.ts`; update it with `pnpm run build:skill` and v
| Quota report | `providers` |
| Provider report | `provider`, `label`, `source`, `windows`, `state`, optional `plan`, and optional `credits` |
| Provider report with `--full` | Optional `account` identity and per-source `attempts` |
| Account identity (`--full`) | Optional `email`, `organization`, `accountId`, and `identityStatus` |

Account identity and per-source `attempts` are omitted unless `--full` is passed.
Claude `identityStatus` is `verified` only when Anthropic returns an authoritative account identifier; `email` and `organization` are display-only and must not be used for duplicate detection.

### Provider `state`

Expand Down Expand Up @@ -293,13 +295,13 @@ Auth source entries can include `credentialPresent` when a non-secret probe conf

### Provider credential sources

| Provider | Credential sources read |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude | `~/.claude/.credentials.json`; on macOS, the `Claude Code-credentials` Keychain value with `--allow-keychain-prompt` or, after a non-secret access marker exists, on plain calls |
| Codex | `$CODEX_HOME/auth.json` or `~/.codex/auth.json` before the read-only CLI fallback |
| Cursor | `$CURSOR_STATE_DB` when set or the platform Cursor state database path |
| GitHub Copilot | `$GITHUB_COPILOT_APPS_JSON` when set or the local Copilot apps auth file |
| Grok | `$GROK_AUTH_JSON`, inline `$GROK_AUTH`, `$GROK_AUTH_PATH`, or `$GROK_HOME/auth.json` / `~/.grok/auth.json` |
| Provider | Credential sources read |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude | `$CLAUDE_CONFIG_DIR/.credentials.json` or `~/.claude/.credentials.json`; on macOS, the corresponding default or path-hashed Claude Code Keychain value with `--allow-keychain-prompt` or, after a profile-scoped non-secret access marker exists, on plain calls |
| Codex | `$CODEX_HOME/auth.json` or `~/.codex/auth.json` before the read-only CLI fallback; `$QUOTA_AXI_CODEX_BINARY` can pin that fallback to an absolute executable path |
| Cursor | `$CURSOR_STATE_DB` when set or the platform Cursor state database path |
| GitHub Copilot | `$GITHUB_COPILOT_APPS_JSON` when set or the local Copilot apps auth file |
| Grok | `$GROK_AUTH_JSON`, inline `$GROK_AUTH`, `$GROK_AUTH_PATH`, or `$GROK_HOME/auth.json` / `~/.grok/auth.json` |

### Provider notes

Expand All @@ -308,11 +310,13 @@ Auth source entries can include `credentialPresent` when a non-secret probe conf
- quota-axi records the non-secret access marker after any successful Keychain value read.
- When that marker exists, plain calls read the Keychain value again so an already-approved "Always Allow" grant keeps live Claude quota fresh.
- Without the flag or marker, quota-axi may perform a non-secret Keychain item presence check so it only suggests Keychain access when a Claude credential item exists.
- After a successful usage read, quota-axi queries Anthropic's first-party OAuth profile endpoint with the same credential. Its authoritative root `account.uuid` is exposed as `account.accountId` only in `--full` output; if that field is absent, `identityStatus` is `unverified` instead of deriving an identity from email, organization data, or cached account metadata.

**Codex**

- Codex `auth.json` support is OAuth-token only; API key values such as `OPENAI_API_KEY` are treated as invalid for quota usage calls and are not sent to ChatGPT usage endpoints.
- It may run `codex -s read-only -a untrusted app-server` for Codex JSON-RPC fallback.
- Set `QUOTA_AXI_CODEX_BINARY` to an absolute executable path when the fallback must use a specific Codex installation. Auth inspection and the app-server probe resolve the same path, and an invalid override fails closed instead of consulting `PATH`.

**Cursor**

Expand Down Expand Up @@ -340,15 +344,15 @@ Auth source entries can include `credentialPresent` when a non-secret probe conf

### Cache

| Item | Behavior |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Quota cache | Lives at `~/.cache/quota-axi/quotas.json` or under `$XDG_CACHE_HOME/quota-axi/` when `XDG_CACHE_HOME` is set. |
| Quota cache permissions | Uses `0600` file permissions. |
| Quota cache contents | Stores normalized non-secret snapshots only. |
| Claude Keychain access marker | Lives alongside the quota cache as `claude-keychain-access-granted`, uses `0600` file permissions, and contains no credential material. |
| Cached reports | Only fresh provider snapshots with windows are cached. |
| Fresh provider reports with no windows | Clear any cached snapshot for that provider, so entitlement-only reports do not leave stale quota windows behind. |
| Reports and details not cached | Failed providers, stale providers, account identity, and source attempts are not cached. |
| Item | Behavior |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Quota cache | Lives at `~/.cache/quota-axi/quotas.json` or under `$XDG_CACHE_HOME/quota-axi/` when `XDG_CACHE_HOME` is set. |
| Quota cache permissions | Uses `0600` file permissions. |
| Quota cache contents | Stores normalized non-secret snapshots only. |
| Claude Keychain access marker | Lives alongside the quota cache as `claude-keychain-access-granted` for the default profile or with an eight-character path-hash suffix for a `$CLAUDE_CONFIG_DIR` profile; uses `0600` file permissions and contains no credential material. |
| Cached reports | Only fresh provider snapshots with windows are cached. |
| Fresh provider reports with no windows | Clear any cached snapshot for that provider, so entitlement-only reports do not leave stale quota windows behind. |
| Reports and details not cached | Failed providers, stale providers, account identity, and source attempts are not cached. |

## Development

Expand Down
6 changes: 6 additions & 0 deletions skills/quota-axi/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ or when comparing supported local provider headroom side by side.
`quota-axi --allow-keychain-prompt` once and approve Keychain access ("Always Allow").
After that successful grant, plain `quota-axi` calls reuse the existing Keychain access
marker to refresh live Claude quota without requiring the flag.
7. For a managed Codex installation, set `QUOTA_AXI_CODEX_BINARY` to its absolute executable
path. quota-axi uses that exact executable for auth inspection and the read-only app-server
fallback, and fails closed if the override is invalid.

## Usage

Expand All @@ -65,6 +68,9 @@ examples:
every provider failed; exit code 2 means a usage error.
- Percentages are not comparable across providers - quota-axi never claims one provider's
percentage equals another's.
- Claude `--full` output exposes the authoritative OAuth profile `account.uuid` as
`account.accountId` when Anthropic returns one; otherwise the account identity is explicitly
marked unverified rather than inferred.
- The quota cache at `~/.cache/quota-axi/quotas.json` only ever holds normalized
non-secret snapshots.
Fresh provider reports with no windows clear stale provider snapshots instead of caching
Expand Down
8 changes: 6 additions & 2 deletions src/lib/fs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mkdirSync, readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { dirname, isAbsolute, join, relative, sep } from "node:path";

Expand Down Expand Up @@ -47,8 +48,11 @@ export function cacheFilePath(): string {
return join(cacheDirPath(), "quotas.json");
}

export function claudeKeychainAccessMarkerPath(): string {
return join(cacheDirPath(), "claude-keychain-access-granted");
export function claudeKeychainAccessMarkerPath(configDir?: string): string {
const suffix = configDir
? `-${createHash("sha256").update(configDir).digest("hex").slice(0, 8)}`
: "";
return join(cacheDirPath(), `claude-keychain-access-granted${suffix}`);
}

function cacheDirPath(): string {
Expand Down
Loading
Loading