Pitch: notch-limits, os limites do Claude Code e do Codex no notch do Mac, lidos do que os CLIs já gravam em disco
Uso o Claude Code com duas contas (a pessoal e a do trabalho, cada uma no seu CLAUDE_CONFIG_DIR) e também o Codex. Para saber qual conta ainda tinha cota, eu rodava /usage em cada terminal. Então fiz um app pequeno para macOS que mostra esses limites como anéis de progresso em volta do notch do MacBook. Passando o mouse, ele expande e mostra cada janela de limite (sessão de 5 h, semanal e semanal por modelo no Claude; 5 h e semanal no Codex), a porcentagem e quando ela reseta.

É open source (MIT), gratuito, feito em Swift + SwiftUI e sem dependências. O DMG tem 3 MB e o zip, 1,7 MB. Não é oficial e não tem vínculo com a Anthropic nem com a OpenAI.
- Repositório: https://github.com/matheusm/notch-limits
- Site (com uma demonstração interativa com dados de exemplo): https://notch-limits.vercel.app
Neste post quero contar as decisões técnicas, porque foram elas que mais me ensinaram.
1. Local-first: o dado já está no disco
A primeira versão óbvia seria consultar a API de uso a cada X segundos. O problema é que esses endpoints têm rate limit, e no Claude o limite é compartilhado com o próprio /usage do CLI. Um app de monitoramento que gasta o rate limit de quem ele monitora não faz sentido.
Acontece que os dois CLIs já gravam o uso em disco:
- Claude Code: guarda a última resposta de uso em
cachedUsageUtilization, dentro do~/.claude.json(ou de$CLAUDE_CONFIG_DIR/.claude.json), com umfetchedAtMs. - Codex: grava um evento
rate_limitsnos arquivos$CODEX_HOME/sessions/AAAA/MM/DD/*.jsonla cada resposta.
Então a regra ficou assim: reler os arquivos locais a cada minuto e só chamar a API quando o dado mais novo tiver mais de 10 minutos, ou quando o usuário apertar o botão de atualizar. Abrir o painel nunca faz requisição. Enquanto você está usando os CLIs, os snapshots deles mantêm o app atualizado sem nenhuma requisição extra.
A decisão central cabe em três linhas (Sources/NotchLimits/NotchLimits.swift):
let best = Usage.newest(previous, local(acc))
let fresh = best?.updatedAt.map { now.timeIntervalSince($0) < staleAfter } ?? false
guard canCall, force || !fresh else { return Outcome(id: acc.id, usage: best, error: nil, calledAPI: false) }
Cada conta mostra a idade do dado ("updated 3 min ago"), e o texto fica laranja depois de 15 minutos. Se o app mostra um número, você sabe de quando ele é.
Ler JSONL sem ler tudo
O histórico de sessões do Codex cresce sem parar. Para não varrer tudo a cada minuto, o leitor olha só as 3 pastas de dia mais recentes, ordena os arquivos por data de modificação, abre no máximo 5 e lê apenas o final de cada um, de trás para frente, até achar a primeira linha com "rate_limits":{:
// o snapshot mais novo está no fim do arquivo; lê só os últimos 512 KB
let size = (try? handle.seekToEnd()) ?? 0
try? handle.seek(toOffset: size > 524_288 ? size - 524_288 : 0)
2. Credenciais: só leitura, e sem prompt do Keychain
Para o fallback na API, o app precisa do token que o CLI já tem. Duas decisões aqui:
Nunca renovar token. Se o app fizesse o refresh do OAuth por conta própria, competiria com o refresh do CLI e poderia invalidar a sessão do usuário. Então, se o token expirou, o app mostra "Token expired — open the CLI to renew it" e espera. Para um app que mexe em credenciais, ser chato aqui é uma qualidade.
Ler o Keychain pelo /usr/bin/security. O Claude Code guarda o login no Keychain. Ler esse item pelo Security framework a partir de outro app (assinado ad hoc) dispara um pedido de permissão. Já o binário security está na lista de acesso do item, então não aparece prompt nenhum. O nome do item segue o esquema do próprio Claude Code, com um sufixo para cada CLAUDE_CONFIG_DIR:
/// Mesmo esquema do Claude Code: dir padrão sem sufixo, demais com sha256(path)[0..<8]
func keychainService(for configDir: String) -> String {
if configDir == "\(home)/.claude" { return "Claude Code-credentials" }
let hash = SHA256.hash(data: Data(configDir.utf8)).map { String(format: "%02x", $0) }.joined()
return "Claude Code-credentials-\(hash.prefix(8))"
}
É isso que permite várias contas: cada config dir tem seu próprio login no Keychain e seu próprio .claude.json. Basta um alias:
alias claude-work='CLAUDE_CONFIG_DIR=$HOME/.claude-work claude'
No Codex, o token vem do $CODEX_HOME/auth.json, também só leitura.
429 e Retry-After. Quando a API responde 429, aquela conta fica bloqueada pelo tempo do header (5 minutos se ele não vier), e o app continua mostrando o último dado bom. Até o botão de atualizar respeita esse backoff.
3. Desenhar em volta do notch
O macOS não tem uma API de "notch", mas tem o suficiente para achar o notch (trecho simplificado de Notch.swift):
let screen = NSScreen.screens.first(where: { $0.safeAreaInsets.top > 0 })
// a área do notch fica entre auxiliaryTopLeftArea e auxiliaryTopRightArea
notchRect = NSRect(x: left.maxX, y: top - height, width: right.minX - left.maxX, height: height)
Por cima disso, um NSPanel sem borda e sem ativação, num nível acima do menu principal, com um fundo preto que "estende" o notch para os lados. Alguns detalhes que deram trabalho:
- Não roubar cliques. Recolhido, o painel ignora eventos de mouse (
ignoresMouseEvents = true), então os cliques chegam na barra de menus normalmente. O hover é detectado consultandoNSEvent.mouseLocationa cada 100 ms. - Não abrir por acidente. O painel só expande depois de 250 ms com o mouse em cima, para não abrir quando você só está passando a caminho de um menu. Ele recolhe 300 ms depois que o mouse sai.
- Sem notch, sem problema. Se nenhuma tela tem notch (tampa fechada com monitor externo, por exemplo), o painel some e o app vira um item na barra de menus.
- O horário de reset mente por um décimo de segundo. A API manda
00:59:59.9para um reset "às 01:00", então o app arredonda para o minuto mais próximo antes de formatar. O formato de 12 h ou 24 h segue o idioma da interface.
4. O que entrou na 1.1
A versão 1.1 saiu em 2 de novembro (changelog: https://notch-limits.vercel.app/changelog). Quatro coisas:
- Contas encontradas sozinhas. O
~/.claudepadrão, qualquer pasta~/.claude<sufixo>com um.claude.jsondentro e qualquer~/.codex*com umauth.jsonviram contas, quando o app abre e sempre que você abre os Ajustes. A descoberta só verifica se esses arquivos existem: não lê o conteúdo deles, nem a configuração do shell, nem o Keychain. Uma janela de Ajustes mostra, esconde, renomeia e reordena as contas, e aplica na hora, sem reiniciar e sem requisição. - Ritmo. Cada barra ganha um traço onde o uso estaria se fosse distribuído por igual pela janela. Se o ritmo atual chega a 100% antes do reset, aparece "Neste ritmo: limite amanhã 00:04". É uma projeção linear a partir do snapshot mais recente, sem histórico guardado. Dois detalhes: a taxa usa o horário em que o percentual foi medido, e não "agora" (senão um snapshot de 9 minutos atrás pareceria mais lento do que é), e essa linha só aparece depois de 15% da janela, porque uma rajada logo no começo projeta números absurdos.
- Melhor agora. Com duas ou mais contas do mesmo provedor, a de menor pico entre as que têm dado com menos de 15 minutos ganha a etiqueta "Melhor agora". Empate ou todas em 100%: ninguém ganha. Claude e Codex não se comparam.
- Notificações, opcionais. Desligadas por padrão. Ligadas, avisam uma vez quando uma janela chega a 90% ("Claude Work está perto do limite") e quando uma janela que estava em 90% ou mais reseta ("Claude Work está disponível de novo"). Só texto, sem som, sem requisição extra, e a primeira leitura depois de abrir o app nunca notifica.
Duas lições das notificações:
UNUserNotificationCenter.current()lança uma exceção Objective-C e derruba o processo quando não existe bundle, como noswift rune noswift test. O app só toca na Central de Notificações seBundle.main.bundleIdentifierexistir, e a regra de quando avisar é uma função pura (uso anterior + uso atual → avisos), testada sem a Central.- O reset vem do horário de reset, não de uma queda do percentual. O snapshot local e a API podem oscilar (90,2% → 89,8%) e discordar do horário por alguns segundos, então um horário até 60 s depois do anterior conta como o mesmo período. Sem isso, o aviso de 90% se repetiria antes do reset.
5. Qualidade num projeto pequeno escrito com IA
Boa parte do código foi escrita com o Claude Code, e eu aprovo cada mudança antes de ela entrar no repositório. Para isso funcionar, as regras precisam ser mecânicas, não convenções:
swiftlint lint --strict(qualquer warning falha),-warnings-as-errorsno compilador e a CI rodando lint, build e testes em todo push e PR.- Testes com Swift Testing. O
Store(que orquestra o refresh) recebe as contas e as fontes de dados como closures injetáveis, então dá para testar "snapshot local velho chama a API" sem rede, sem Keychain e sem tocar no~/.claudede verdade. O mesmo vale para a descoberta de contas, o ritmo e as notificações: funções com o relógio e a pasta home injetados.
6. Distribuição
O app ainda não é notarizado. Isso exige o Apple Developer Program (US$ 99 por ano), e preferi lançar antes; ele é assinado ad hoc. O instalador de uma linha baixa o zip do último release com o curl para ~/Applications e remove o atributo de quarentena, por isso não aparece o aviso do Gatekeeper. Leia o script antes, ele é curto: https://github.com/matheusm/notch-limits/blob/main/install.sh
curl -fsSL https://raw.githubusercontent.com/matheusm/notch-limits/main/install.sh | bash
Quem preferir o DMG precisa, na primeira abertura, ir em Ajustes do Sistema → Privacidade e Segurança e clicar em Abrir Mesmo Assim.
Limitações (sendo honesto)
- Os endpoints de uso não são documentados e podem mudar sem aviso.
- Só macOS 14 ou mais novo, e só Claude Code e Codex.
- No Codex, o app lê o login do
auth.json, que é onde o Codex guarda por padrão. Se você configuroucli_auth_credentials_storecomokeyringouauto, o login fica no cofre de credenciais do sistema e o app não encontra essa conta. - Se você quer um app para muitos provedores, o CodexBar, do Peter Steinberger, cobre bem mais: barra de menus, muito mais provedores, Homebrew, CLI, e também tem notificações e ritmo. Ele também mostra várias contas do Claude, cada uma como uma conta de token que você cola (um cookie
sessionKeyou um token OAuth) ou um perfil do claude-swap. O foco aqui é outro: o notch-limits acha sozinho cada login deCLAUDE_CONFIG_DIR, lê as credenciais daquele login sem alterar nada, não pede nada para colar e fica no notch.
Próximos passos
- Um tap do Homebrew.
- Notarização, se o app tiver uso suficiente para justificar a taxa da Apple.
- Histórico de uso e outros provedores, só se as pessoas pedirem.
Uma pergunta para vocês
Alguém conhece um jeito melhor de detectar hover perto do notch do que consultar a posição do mouse a cada 100 ms? Mantive o painel recolhido ignorando eventos para não atrapalhar a barra de menus, e o polling foi o caminho mais simples que achei. Críticas ao código são bem-vindas: https://github.com/matheusm/notch-limits
A interface está em inglês, português e espanhol (segue o idioma do macOS).