SLOPSHOPPER

account-switch

Switch the Claude subscription account when a rate-limit window is nearly full, and back once it resets

newtoastprocesstimer
★ 1v0.1.0MITupdated 2026-10-25vladkens/dotfiles/claude/account-switch
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · account-switch
› fix the failing auth test and add an audit log call ╭────────────────╮ │ account-switch │ ⏺ Read(src/auth.ts) │ dev │ ⎿ Read 6 lines ╰────────────────╯ ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

dotfiles

Personal application configs and agent settings, managed with Dotter.

Setup

Install Homebrew, clone this repository, then install Dotter:

brew install dotter

Create .dotter/local.toml (ignored by Git) to select a profile:

packages = ["air-m5"]

Available profiles:

  • air-m5
  • pro-m2

Profile contents are defined in .dotter/global.toml.

develop installs rustup, uv, and pnpm, configures Fish paths, and installs Rust stable, the default stable Python supplied by uv, and Node.js LTS.

containers installs Docker, Compose, Buildx, and Colima. Start the container engine with colima-up (4 CPUs, 8 GiB, Rosetta); stop it with colima stop.

Preview config changes, then install the selected groups and link their configs:

dotter deploy --dry-run
make deploy

make deploy can overwrite existing target files (--force).

Files

  • .dotter/global.toml: profiles, groups, and config destinations.
  • .dotter/Brewfile-*: programs to install for each group.
  • dotfiles/: application configs.
  • claude/, codex/: agent settings and extensions.
  • bin/: personal CLI tools; scripts/: repository utilities.

Codex Config Notes

Opened issues:

Network

network.enabled = false: commands have no network access. Commands matched by prefix_rule(..., decision="allow") are the exception: they bypass the sandbox and have network access (probably filesystem too, since this is a sandbox bypass).

network.enabled = true: network access is enabled for all commands.

There is also an option to restrict traffic by domains. It requires all three: network.enabled = true, network.mode = "limited", and features.network_proxy = true. Then traffic is filtered by network.domains. Current caveat: even prefix_rule(... allow) commands are routed through the proxy, so an allowlisted command fails if its domain is not allowed there.

Note: network.mode = "limited" is silently ineffective without features.network_proxy = true.

Filesystem

Denied reads under [permissions.<profile>.filesystem.":workspace_roots"] disable the unsandboxed part of rules.allow:

[permissions.dev_workspace.filesystem.":workspace_roots"]
"**/*.env" = "deny"

With any filesystem deny, Codex does not run allowlisted commands outside the sandbox. The rule still matches, and the approval prompt can be skipped, but execution stays sandboxed because denied reads only work inside the sandbox.

Practical result: rules.allow stops being useful as a sandbox bypass. Commands that need denied files, such as .env, fail even when the command is allowlisted.

Source 1 files
hooks/register.ts 56 lines
1import type { EngineInterface, Register, SessionRateLimit } from "claude-code"
2
3// Readings only wake a verified check. Keep work pending until Python acknowledges it.
4let last = "[]"
5let pending = false
6let running = false
7
8function key(limits: readonly SessionRateLimit[]) {
9  return JSON.stringify(limits.map(limit => [limit.kind, limit.percentUsed, limit.resetsAt]))
10}
11
12async function check($: EngineInterface, limits: readonly SessionRateLimit[], changed = false) {
13  const reading = key(limits)
14  if (changed || reading !== last) pending = true
15
16  last = reading
17  if (!pending || running) return
18
19  running = true
20  pending = false
21  try {
22    const script = `${$.plugin.root}/account-switch.py`
23    const run = await $.process.run(["uv", "run", "--script", script, "measure"], {
24      stdin: JSON.stringify(limits),
25      timeoutMs: 60_000,
26    })
27    if (run.exitCode !== 0) pending = true
28
29    const text = (run.stdout || run.stderr).trim()
30    if (text) $.ui.toast(text)
31  } catch {
32    pending = true
33    $.ui.toast("Account usage check could not run; will retry")
34  } finally {
35    running = false
36  }
37}
38
39export const register: Register = on => {
40  on("session.measure", async ($, e, next) => {
41    const result = await next(e)
42    if (e.changed.includes("rateLimits")) await check($, e.rateLimits, true)
43
44    return result
45  })
46
47  // Retry skipped checks even if a blocked session produces no more responses.
48  on("session.start", async ($, e, next) => {
49    const result = await next(e)
50    $.clock.every(60_000, async () => check($, (await $.session.usage()).rateLimits))
51    await check($, (await $.session.usage()).rateLimits)
52
53    return result
54  })
55}
56