npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

backstay

v0.1.2

Published

Instala o Backstay num repositório: traduz hooks do Claude Code em eventos, em menos de 3 minutos, sem conta no modo --local.

Readme

backstay

Traduz os hooks do Claude Code em eventos, para você saber o que seus agentes fizeram.

Do comando ao primeiro evento visível: menos de 3 minutos. No modo local, que é o padrão, sem conta nenhuma.

npx backstay init

Sem flag nenhuma ele instala em modo local e diz que escolheu isso por você, o que implica e como mudar. Um padrão silencioso seria o defeito que este programa existe para acusar.

Versão pré-1.0: a interface pode mudar entre versões 0.x.

Ainda não há repositório público nem rastreador de issues. O desenvolvimento acontece num repositório privado, e este pacote é um recorte dele. O código-fonte vai inteiro aqui dentro, sob Apache-2.0: npm pack backstay e você lê tudo o que roda na sua máquina — são 13 arquivos em texto puro, sem passo de build e sem dependência.

Dizemos isso em vez de deixar você procurar um link de "Repository" que não existe.


🔒 O que este programa NUNCA lê e NUNCA envia

Esta é a primeira seção do README de propósito: é a pergunta que um engenheiro de plataforma faz antes de qualquer outra, e ela merece uma resposta verificável, não uma promessa.

Nunca lidos, nunca enviados:

  • ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN
  • credenciais de cloud — AWS_*, GOOGLE_APPLICATION_CREDENTIALS, AZURE_OPENAI_API_KEY, …
  • conteúdo de qualquer arquivo do seu projeto
  • o texto das mensagens do modelo (vai o tamanho em caracteres, não o texto)
  • a URL completa do seu gateway ou do seu git remote (vai só o hostname)

O que é enviado: metadado de evento — tipo (SessionStart, Stop, …), id de sessão, diretório, os dois carimbos de tempo, e um payload recortado por evento.

Como conferir em vez de acreditar

  1. Leia o programa inteiro. Depois de instalar, .backstay/backstay-hook.mjs é o arquivo que roda a cada evento. Ele não tem dependência nenhuma e cabe numa tela e meia. É uma cópia verbatim de src/hook-runtime.mjs.
  2. A superfície da coleta é uma função só. recortarPayload (em hook-runtime.mjs) tem um switch por tipo de evento, e o default devolve {} — evento desconhecido emite metadado vazio em vez de repassar o payload bruto. O que não está escrito ali não sai da sua máquina.
  3. process.env é lido em exatamente um lugar no runtime do hook, e é uma flag nossa.
  4. A regra é testada, não declarada. tests/prova-adaptador.mjs monta um ambiente com todas as variáveis sensíveis preenchidas com valores-sentinela e afirma que nenhum deles aparece na saída — incluindo uma URL de gateway com senha e chave embutidas. E, para o verde valer, há uma perturbação deliberada: um metadado propositalmente vazado, que o mesmo critério tem de acusar. Guarda que nunca reprovou não é guarda.

Por que isso importa para você: é o que permite instalar isto numa empresa regulada sem abrir uma exceção de segurança. Não é preferência de design — é requisito.


O que o init faz, em ordem

  1. Detecta o ambiente — Claude Code presente, settings.json de projeto ou de usuário, git remote, e o provider (Bedrock, Vertex, Foundry, gateway, ou direto).
  2. Declara o modo de instalação e por quê — veja "Os hooks valem para quem clonar", abaixo.
  3. Mostra o diff literal do que vai escrever, e pergunta uma vez. Nunca escreve calado.
  4. Escreve preservando os seus hooks. Handler alheio nunca é tocado, e grupo com matcher que não é nosso nunca é usado.
  5. Emite um evento de teste e confirma que ele chegou onde deveria — no buffer (modo local) ou confirmado pelo hub com 2xx (modo hospedado). No hospedado, evento que cai no buffer é falha de entrega e sai com exit ≠ 0: o buffer ali é a rede de segurança, não o destino.
  6. Imprime a URL com o evento já lá.

npx backstay eject desfaz tudo — e prova por diff que o settings.json voltou ao estado anterior, inclusive quando você já tinha hooks antes. Se o .claude/settings.json não existia antes de nós e ficou vazio, ele é apagado em vez de ficar como um {} para você remover à mão; a pasta sai junto se ficou vazia. O que já era seu nunca é tocado. O buffer fica: apagá-lo não é desinstalar, é perder dado.

Onde ele escreve — e o que ele NUNCA faz sem você pedir

No .claude/settings.json da pasta em que você o rodou. Se ali houver um repositório git, na raiz dele; se não houver, na própria pasta — o Claude Code lê o .claude/ do diretório em que é iniciado, com ou sem git, e é isso que o init usa.

🔒 O settings.json de usuário — o global, que vale para TODOS os seus projetos — só é tocado quando você digita --usuario. Não achar um repositório git nunca é motivo para escrever no global: escopo global é decisão sua, e decisão sua a gente pede, não assume. E se você rodar o init dentro da sua própria pasta de usuário, onde os dois escopos são o mesmo arquivo, ele recusa e explica em vez de escolher por você — inclusive com --sim.

Os hooks valem para quem clonar — e agora isso é verdade

No settings.json de projeto, o caminho do hook é relativo (.backstay/backstay-hook.mjs), com / mesmo no Windows. Esse arquivo é versionado, então um caminho absoluto não sobreviveria ao git clone: seu colega receberia um hook apontando para um diretório que não existe na máquina dele, e nada avisaria. (No settings.json de usuário o caminho é absoluto de propósito — ele vale para todos os seus projetos, e um relativo ali apontaria para o repositório aberto no momento.)

O init diz qual modo detectou, e a declaração não é cortesia:

| modo | quando | o que ele declara | |---|---|---| | node | há package.json na raiz | que não declara dependência nossa no seu package.json — o hook é autocontido e funciona sem node_modules | | poliglota | não há package.json | que o hook é um programa Node, e portanto node precisa estar no PATH |

A segunda linha existe porque assumir Node em silêncio num repositório Java, Python ou Go é a forma de descobrir o requisito depois que ele já falhou.

⚠️ O caminho relativo só resolve se o arquivo viajar junto. Se o seu .gitignore ignora o .backstay/, o init avisa 🔴 — e o doctor, na máquina de quem clonou, acusa.

Já tem hooks de telemetria nos mesmos eventos?

O init avisa antes de escrever, dizendo quais eventos, quantos emissores e qual é o comando de cada um. Por padrão ele acrescenta e convive — é a promessa "só adiciono", e é ela que torna este instalador aceitável na sua máquina.

Se você preferir que ele substitua o caminho antigo em vez de conviver:

npx backstay init --local --substituir

É a única coisa que este programa faz além de acrescentar, e por isso é opt-in:

  • antes de você usar a flag, o init já lista nominalmente o que ela removeria — convite sem a consequência declarada é o mesmo silêncio, com um passo de confirmação em cima. O doctor mostra a mesma lista a qualquer momento;
  • cada remoção aparece como linha - no diff, antes de você confirmar;
  • gate nunca é tocado — nem se algo pedir. Hook de política é o que menos pode sumir em silêncio;
  • hook alheio que não emite telemetria (um formatador, um linter) também não é tocado;
  • eject devolve cada um deles à posição original, e continua provando diff vazio.

A detecção do que é telemetria é heurística sobre o texto do seu comando — provável, não certa. Confira o diff. Se algo ali não for telemetria, cancele.


backstay doctor — o controle está ativo AGORA?

npx backstay doctor

A pergunta não é "foi instalado?" — isso o init respondeu uma vez, no passado. É sobre o presente, e ela existe porque o modo de falha que mais dói não é "não funciona": é "parou de proteger e ninguém avisou". Um controle que nunca funcionou é consertado na terça. Um controle que parou, continuando a aparecer no seu settings.json e no seu diagrama de arquitetura, é descoberto durante uma auditoria.

Numa tela, o doctor responde:

  • quais hooks estão declarados — e quais são nossos, quais já estavam lá;
  • 🔴 algum hook aponta para um arquivo que NÃO EXISTE nesta máquina? — ele está declarado e não roda: nada falha, nada reclama. É o caso mais puro de "parou de proteger e ninguém avisou", e o vetor mais comum dele é o git clone (veja abaixo);
  • 🟡 algum caminho é ABSOLUTO num arquivo VERSIONADO? — acusa mesmo quando o arquivo existe aqui, porque é aqui que ele existe: o defeito aparece na máquina de quem clonar;
  • 🔴 algum evento tem MAIS DE UM EMISSOR? — o caso que ninguém vê, porque não gera erro nenhum;
  • 🟡 o que --substituir REMOVERIA — nominalmente, antes de você decidir usá-lo;
  • algum gate está declarado async? — se estiver, ele não está barrando nada;
  • o buffer está drenando ou acumulando — buffer parado é entrega atrasada;
  • 🟡 QUAL CÓDIGO O SEU HUB ESTÁ RODANDO — ele pergunta a identidade ao hub e a compara com o repositório: quantos commits atrás está, ou se o hub sequer sabe responder. Identidade declarada e nunca comparada não é controle, é decoração;
  • 🟡 este adaptador é a última versão publicada? — adaptador velho diagnostica com as regras da época dele;
  • onde ele procurou — as três camadas de settings.json, sempre listadas, inclusive as que não existem, e se cada uma vai ou não para o git.

Quando ele não sabe, ele diz que não sabe. Caminho que não deu para conferir, git que não respondeu, hub fora do ar, buffer sem modo declarado: nada disso vira achado — vira uma linha dizendo que a conferência não aconteceu. Julgar por dúvida é o defeito que este comando existe para combater; calar sobre a dúvida é o outro.

Ele só lê. Não escreve e não altera nada. Ele faz duas perguntas pela rede, e nenhuma delas carrega qualquer coisa sua: "que código você está rodando?" ao seu próprio hub, e "qual é a última versão deste pacote?" ao registro público do npm. Teto de 3 s cada, em paralelo; falha de rede nunca vira acusação. --sem-rede desliga as duas — e o relatório diz que não conferiu, para que a ausência de acusação não seja lida como "está em dia".

Ele distingue certeza de palpite, e diz qual é qual

| | O que é | Como ele sabe | |---|---|---| | nosso | CERTO | a marca no comando é dado nosso | | emissor preexistente | PROVÁVEL | heurística sobre o texto do seu comando | | convivendo | informativo | hook alheio sem sinal de telemetria — não é acusação |

O terceiro nível existe porque um diagnóstico que grita em todo repositório é ignorado em todos — e aí o silêncio volta pela porta dos fundos. Um formatador declarado em Stop não duplica evento nenhum, e apontá-lo seria inventar achado.

Exit ≠ 0 só para o que é certo (duplicação nossa, gate async). O que é heurística informa e não reprova: um comando que falha CI por palpite é desligado, e um diagnóstico desligado não diagnostica.


O desenho, e por que ele não é o óbvio

O init escreve handler type: "command" com async: true, e é o próprio comando que faz o POST, com timeout curto, caindo para um .jsonl local quando o POST falha.

O caminho óbvio seria type: "http" apontando direto para o servidor. Ele foi medido e eliminado, por um motivo que não é a latência:

O hook http é executado pelo próprio Claude Code. Se o servidor estiver fora do ar, ninguém escreve o buffer e o evento simplesmente some.

Com command, o pior caso é o evento ficar no disco esperando a drenagem — uma linha do tempo atrasada, não uma linha do tempo furada. O drenador deixa de ser o caminho normal e vira caminho de recuperação.

Dois fatos que economizam sua pesquisa, porque a documentação é fácil de ler errado:

  • async e asyncRewake são campos exclusivos do hook command. O hook http não os aceita.
  • Não existe asyncTimeout. O campo chama-se timeout.

⚠️ async serve a hooks de OBSERVAÇÃO, nunca a hooks de GATE

Um hook assíncrono não barra a chamada de ferramenta. Se você tem um hook de política em PreToolUse — um gate, um aprovador, um bloqueador —, ele precisa continuar síncrono: declarado async, ele deixa de decidir em silêncio e continua parecendo ativo.

O adaptador nunca escreve gate, e recusa a instalação se o resultado tivesse um gate declarado async (gatesDeclaradosAsync, em src/config-logic.mjs).


Modo local — o padrão

npx backstay init            # o mesmo que `init --local`

Grava em .backstay/eventos.jsonl e sobe um painel local. Sem conta, sem token, sem rede. É o caminho para ver se isto vale alguma coisa antes de decidir qualquer coisa.

O modo hospedado (--hub <url> --token <t>) exige credencial, porque é estado compartilhado — e a recusa vem antes de qualquer escrita, dizendo isso. Hoje não há cadastro aberto no hub hospedado: quem emite a credencial é quem opera o hub. Está escrito aqui em vez de existir uma página de cadastro que não existe.

O painel sempre declara onde procurou — inclusive quando não encontrou nada. Instrumento que responde "nada encontrado" sem dizer onde olhou é indistinguível de instrumento que olhou no lugar errado.


Comandos

| | | |---|---| | backstay init | instala em modo local (o padrão), sem conta — e declara que escolheu | | backstay init --hub <url> --token <t> | instala apontando para um hub; o token é obrigatório | | backstay doctor | o controle está ativo agora? — só lê, e não toca a rede | | backstay eject | remove, provado por diff, preservando os seus hooks | | backstay drenar | varre todos os buffers do repositório e declara onde procurou | | backstay painel | sobe só o painel local |

Se a sua tela mostrar caracteres estranhos

Num console Windows com code page legada (≠ 65001), o adaptador rebaixa a saída sozinho para ascii — sem emoji e sem acento — e declara que fez isso na primeira linha. Nenhuma mensagem deste programa depende de emoji para ser compreendida: emoji aqui é ênfase, jamais significado.

Para mandar no nível você mesmo:

BACKSTAY_SAIDA=completo    # emoji e acento
BACKSTAY_SAIDA=sem-emoji   # sem emoji, com acento
BACKSTAY_SAIDA=ascii       # só ASCII

Comuns: --sim (não pergunta — para CI), --usuario (settings do usuário), --porta <n>, --sem-painel (não deixa o painel de pé). Só do init: --substituir (remove emissor preexistente em vez de conviver — opt-in, visível no diff, reversível pelo eject).

--help imprime isto e sai, sem executar nada.


Requisitos

Node 18+. Roda em Windows, macOS, Linux e dentro de container de CI.

Licença: Apache-2.0.