sshepherd
v0.0.10
Published
Framework de agentes que leva um dev sem experiencia de sysadmin do zero absoluto ate uma aplicacao publicada em VPS proprio, com servidor endurecido, deploy versionado e rollback em um comando.
Maintainers
Readme
SSHepherd
Do "não tenho servidor" ao "publiquei e sei voltar atrás", sem virar sysadmin.
SSHepherd é um framework de agentes que instala na pasta da sua aplicação e cuida do seu VPS: escolha do provedor, criação da máquina, chave SSH, endurecimento, provisionamento, deploy versionado, rollback e diagnóstico.
Ele foi feito para uma pessoa específica: quem sabe programar e não sabe administrar servidor.
Instalação
cd sua-aplicacao
npx sshepherd installDepois, abra seu agente (Claude Code, Codex ou compatível) e digite:
/sshpSe você ainda não tem servidor, ele começa pelo Dia Zero e te acompanha até o SSH funcionar.
Requisitos: Node 18.20.2 ou superior, cliente OpenSSH (nativo em Windows 10+, macOS e Linux). O alvo remoto precisa ser Ubuntu 22.04/24.04 ou Debian 11/12.
Comandos do instalador
npx sshepherd install # instala nesta pasta
npx sshepherd update # atualiza agentes e scripts, preservando seus dados
npx sshepherd status # estado da instalação, do Dia Zero e dos servidores
npx sshepherd uninstall # remove, com opção de preservar perfis e logsEngines suportadas
Claude Code, Codex, Cursor, Gemini CLI, Windsurf, Antigravity, Kiro, Opencode, Cline, Roo Code, GitHub Copilot, Aider e Amazon Q Developer. O instalador detecta quais estão presentes — pela pasta do projeto ou pelo binário no PATH — e já vem com elas marcadas.
Aviso de nova versão
A instalação registra um hook SessionStart no Claude Code que roda .sshepherd/bin/version-notice.js. Ele lê só o cache local, sai em ~130ms e nunca bloqueia a sessão; a checagem de rede acontece em processo destacado, valendo para a sessão seguinte. Quando há versão nova, o agente pergunta se pode atualizar — nunca atualiza sozinho. Nas engines sem hook, a mesma instrução vai no entry file.
O que ele faz
| Comando | O que faz |
|---|---|
| /sshp | Descobre em que ponto você está e roteia |
| /sshp-setup | Dia Zero: provedor → máquina → chave → usuário não-root → servidor fechado |
| /sshp-connect | Perfis de servidor, teste de conexão, histórico de auditoria |
| /sshp-harden | Auditoria de segurança com nota 0–100 e correções |
| /sshp-attack | Olha seu servidor do lado de quem ataca: o que responde e o que vaza |
| /sshp-provision | Detecta sua stack e prepara o servidor |
| /sshp-secrets | Chaves de API e senhas no cofre do servidor, sem passar pelo chat |
| /sshp-deploy | Publica, verifica saúde, volta versão |
| /sshp-doctor | Diagnóstico quando algo para de funcionar |
| /sshp-agents-help | Catálogo dos agentes |
O que sobe para o servidor
Sua pasta tem coisa que é sua e não do servidor: especificação, prompt de agente, material interno, planilha de preço. Mandar a pasta inteira transforma tudo isso em arquivo público.
Quem decide é o .sshepherdignore na raiz do projeto, criado na instalação, uma regra por linha no formato do rsync:
.env # segredo
.reversa/ # frameworks de IA: especificacao e prompt
_reversa_*/
.claude/
.sshepherd/
.git/ # historico carrega segredo ja removido
node_modules/ # reconstruido no servidor
/aula/ # seu: material internoTrês coisas:
- O Dia Zero termina aqui. O estágio 8 do
/sshp-setuplista sua raiz, compara com o arquivo e pergunta item por item o que ele não tem como saber —aula/é parte do site ou é seu material? Fecha mostrando os dois lados, o que vai e o que fica. - Existe um piso que você não derruba.
.env,.env.*,.git/,.sshepherd/,*.pem,*.key,id_rsa,id_ed25519são excluídos pelo script sempre, apagar a linha não adianta. Sem o arquivo, o deploy recusa publicar. - Na estratégia
gitele não vale. O servidor clona do repositório, então quem manda é o.gitignore. Se.reversa/está versionado, ele chega no servidor. Os agentes avisam antes.
Chaves e segredos
A chave da OpenAI, a senha do banco, o token do Stripe. O risco com um agente de IA operando o servidor não é só alguém de fora: é a chave entrar na conversa, por acidente ou por instrução injetada num log, e dali para onde a conversa for. O SSHepherd é desenhado para que o valor de um segredo nunca entre no contexto do modelo, e isso é feito em camadas que não dependem umas das outras:
- No servidor, o cofre.
/var/lib/sshepherd/secrets/app.env, root:root 600, dentro de uma pasta 700. A aplicação roda como um usuário próprio,sshepherd-app, sem senha e sem SSH, e recebe as variáveis do systemd, que lê o cofre antes de trocar de usuário. O usuáriodeploy, que é o que o agente usa, não lê o arquivo: só consegue chamarsshepherd-secrets, um executável de root com subcomandos fechados, pela única regra de sudo que recebe para isso. Ele responde com nomes, nunca com valores. - A chave entra pelo teclado da pessoa.
secrets.sh --action setabre uma sessãossh -tno terminal dela e o helper pede o valor duas vezes, sem eco. Rodado pelo agente, sem terminal, o script imprime o comando e sai. Não existe--value, não existeget. - Segredo que não precisa existir em outro lugar nasce no servidor.
generatecria chave de sessão e senha de serviço com/dev/urandom; a senha do banco já vem assim dodb.sh. O valor nunca viaja. - Aplicar é transacional.
apply --healthreinicia o serviço, espera ele ficar ativo e estável, bate no endereço de saúde. Com saúde, o cofre vira "o último que funcionou". Sem saúde, volta para o anterior, reinicia de novo e dizapply=revertido. Uma reversão, nunca um laço; lock contra operação concorrente; toda troca por temporário emv, sem instante sem cofre. - Comando livre é de pessoa.
ssh-exec.sh --rawe--op X --argexigem o identificador do perfil digitado no teclado, em qualquer ambiente e com qualquer política. Shell livre no servidor é o que tornaria todo o resto inútil, e regex de proibição não segura shell. - Na máquina local, o hook. Além de
sshcru, ele negaRead,Grep,Edit,WriteeBashem.env,.env.*, chave privada,.npmrc,credentials.json, e nega escrita em.sshepherd/scripts/..env.examplepassa, porque não tem valor. - O que sai do servidor passa pela redação.
redact.sh, compartilhado por executor, deploy, db e secrets: nome por sufixo (_KEY,_SECRET,_TOKEN,_PASSWORD,_URL), valor entre aspas, credencial em URL, formatos conhecidos (sk-,ghp_,AKIA, JWT), chave privada em várias linhas. Redige antes de cortar o resumo do log. É a última rede, não a primeira. - Backup não leva segredo. O cofre não entra no backup, o
.envde compose é excluído, epullsó traz arquivo com nome de backup, só com teclado, só para fora da pasta do projeto.
O fluxo para a pessoa: /sshp-secrets mostra o que existe e o que falta (comparando com o .env.example), entrega o comando para cadastrar, gera o que pode ser gerado, aplica com reinício verificado e confere.
Onde o cofre não chega
- Código que roda com os segredos pode imprimi-los. Quem publica código que faz
console.log(process.env)vê tudo, em base64 se quiser, por fora de qualquer redação. O cofre protege contra leitura direta e vazamento acidental; o código que sobe é responsabilidade de quem aprova o deploy. - Compose fica fora. A unidade de compose roda o YAML da release, e YAML de quem publica rodando como root seria root para quem publica. Em compose os segredos continuam em
shared/.env, legíveis pordeploy. - O provisionamento ainda precisa de root por
sudo -n bash -s, e o sudoers que o framework gera não concede isso. Num servidor novo, o cofre é instalado no Dia Zero, na sessão de root que o framework já usa. Num servidor antigo,secrets-install.sh --printimprime o comando para colar no console do provedor como root. Se você liberou sudo amplo paradeploypara o provisionamento funcionar, a fronteira do cofre passa a depender do teclado no comando livre e das lombadas no--build. - Teclado é proteção operacional, não autenticação. Um agente com shell local consegue, em tese, abrir um pseudoterminal. O que segura de verdade é o cofre pertencer a root.
- O hook existe no Claude Code. Nas outras engines a camada local é a instrução no agente.
O hacker do bem
/sshp-harden pergunta ao servidor como ele está configurado. /sshp-attack não pergunta nada: bate na porta e vê o que responde.
As duas coisas divergem mais do que parece. Firewall ativo e configurado certo, com o Postgres aberto para a internet do mesmo jeito — porque o Docker publica porta por cima do ufw e a regra do firewall não vale para ela. O harden dá nota alta. O attack mostra o banco exposto.
Três lentes:
- De fora, sem chave — que portas respondem pela internet. Banco de dados aberto é o achado mais comum e o mais grave; Redis e Mongo abrem sem senha por padrão.
- O que a aplicação entrega —
/.env,/.git/HEAD,docker-compose.yml, dump de banco, e a documentação do projeto servida como página. - De dentro — permissão do arquivo de segredos, material interno publicado junto com o código, socket do Docker acessível, sudo sem senha.
E o vetor que é novo: a varredura ficou barata. Mapear um alvo era trabalho de horas de gente com paciência; hoje roda sozinho contra milhares de endereços ao mesmo tempo. Por isso um _reversa_sdd/prd.md exposto deixou de ser inofensivo — ele descreve sua regra de negócio em texto escrito para ser entendido. Era papel, virou mapa.
A base de ameaças
O que procurar não é lista fixa — uma lista de dois anos atrás procura buraco que já foi tapado e ignora o que está sendo usado agora. A base fica em .sshepherd/threats.db, uma linha por ameaça, legível a olho nu:
caminho|/.mcp.json|critica|conexoes do seu assistente de IA|Guarda os tokens...|<fonte>|2026-08-06
porta|9000|alta|painel do Portainer|Quem entra ali manda na maquina.|<fonte>|2026-08-06O agente atualiza pesquisando na web, filtrando pela stack do projeto, e sempre mostra antes de gravar — com a fonte de cada item. Reinstalar não apaga o que ele aprendeu. Quando a base passa de 60 dias, o script avisa.
Só entra o que dá para detectar pedindo um endereço ou batendo numa porta. Nada que explore.
Os limites
Só examina servidor registrado em .sshepherd/servers/, e recusa qualquer outro endereço mostrando quais são os seus. Não explora, não tenta senha, não mede carga, não altera nada.
O pastor acha o buraco na cerca e chama o dono. Não atravessa, e não vai olhar a cerca do vizinho.
O Dia Zero
É a parte que os tutoriais não cobrem e onde a maioria desiste. O agente conduz do zero absoluto, aceita print de tela do painel do provedor para saber onde você está, e valida cada passo por comando — não por "parece que deu certo".
Termina quando cinco condições estão comprovadas:
- você entra por chave, como usuário que não é root
- login de root pela rede desligado
- login por senha desligado
- firewall ativo negando entrada
- atualizações automáticas de segurança ligadas
Provedores cobertos em detalhe: DigitalOcean, Hostinger, Contabo, Hetzner. Outros recebem o roteiro genérico.
Arquitetura
Três camadas, separadas de propósito:
npm (bin/, lib/) → só instala e atualiza. Não toca em servidor.
agents/ (markdown) → decidem, conversam, julgam contexto.
scripts (.sshepherd/) → executam no servidor. Determinísticos e auditáveis.O agente é bom em julgar contexto e ruim em garantir determinismo. O script é o oposto. Por isso nenhum comando remoto é improvisado pelo modelo: tudo passa por script versionado, que você pode ler antes de rodar.
A trava que faz essa regra valer
Regra escrita no prompt não segura ferramenta. Bastava o agente chamar ssh direto e a camada de controle ficava de fora, sem deixar rastro. Por isso a instalação registra um hook PreToolUse (.sshepherd/bin/guard-hook.js) que recusa ssh, scp, sftp, rsync, ssh-keyscan, ssh-copy-id e ssh-add crus antes de rodarem. Ele quebra a linha nos operadores, inclusive $( e crase, e julga cada trecho sozinho, então esconder o comando depois de um && ou dentro de uma substituição não passa. Um trecho só é "do framework" quando a palavra de comando é bash, sh ou powershell seguida do caminho em .sshepherd/scripts/: o caminho num comentário não libera nada. O mesmo hook nega leitura e escrita de arquivo de segredo local e escrita em .sshepherd/scripts/. Evento ilegível é recusado, não liberado.
Dentro do executor (ssh-exec.sh), quatro freios sustentam o resto:
- Risco só sobe, nunca desce. Cada operação tem um nível (leitura, alteração, destrutivo) e quem chama pode declarar um nível maior, jamais menor. Comando cru entra com piso
alteração. - Destrutivo em produção exige teclado.
--confirmedé afirmação de quem chama, e quem chama pode ser um agente que alucinou a confirmação. O script abre/dev/ttye espera uma pessoa digitar o identificador do perfil. Sem terminal ligado, ele recusa. - Comando livre exige teclado, sempre.
--rawe--op X --argsão shell arbitrário no servidor. O agente roda as operações fixas (whoami,osinfo,disk,releases,logs) e os scripts; o resto a pessoa roda, e o script diz isso com o comando pronto. - Sudoers sem curinga. O usuário
deployrecebe sudo sem senha só para unidades nomeadas, uma lista fixa de pacotes e o helper do cofre, porquesystemctl restart *aceitariastop sshd.serviceejournalctl *abre shell de root pelo paginador.
Onde essa camada ainda não chega
Sendo honesto sobre o estado atual, porque num framework de segurança a diferença entre o que está pronto e o que está planejado importa mais que a lista de recursos:
- Os scripts operacionais ainda não passam pelo executor.
deploy,harden,provision,db,restore,rollbackebackupabremsshdireto. São scripts versionados, que você lê antes de rodar, e o hook os reconhece. Mas a classificação de risco, o portão de confirmação e o log de auditoria vivem nossh-exec.sh, e ligar essas operações a ele é trabalho em aberto. Enquanto não estiver feito, o log cobre o que passa pelo executor, não toda operação remota. - O hook enxerga a linha de comando. Um agente que escreve um script local com
sshdentro e executa esse script passa por fora (desde que o script não esteja em.sshepherd/scripts/, onde ele não consegue mais escrever); ali quem segura é o sudoers restrito. Para arquivo de segredo vale o mesmo:cat $(ls -a | grep env)passa pelo hook, e o que segura é o cofre pertencer a root no servidor. - O hook é registrado no
settings.jsondo Claude Code. Nas outras engines a camada de controle depende da instrução no agente e do sudoers. - A paridade com PowerShell é parcial. O
ssh-exec.ps1existe, os demais scripts ainda não têm contrapartida, e ele não lê oconfig.toml.
As regras que o framework não quebra
- Nada muda sem você ver o comando. Toda operação que altera o servidor mostra o comando exato e o alvo antes, e espera confirmação. Leitura e diagnóstico rodam livres.
- Comandos destrutivos são recusados sempre. Apagar o disco, parar o SSH, resetar o firewall — não há confirmação que libere.
- Nenhum caminho de acesso é fechado antes de o novo estar provado. É o que impede você de ficar trancado para fora do próprio servidor.
- A chave privada nunca é lida. O framework passa o caminho ao cliente SSH, nunca o conteúdo.
- Tudo fica registrado. Cada comando remoto vai para
.sshepherd/logs/, com segredos redigidos — inclusive o que foi recusado ou bloqueado. - root é usado uma única vez. No primeiro acesso, para criar o usuário
deploye instalar o cofre. Depois disso, nunca mais. - O valor de um segredo nunca entra na conversa. Nem pedido, nem aceito, nem lido de
.env. Vai do teclado da pessoa para o cofre do servidor, e o agente só vê nomes.
Deploy e rollback
Publicação em releases/<timestamp> com troca atômica do symlink current:
/srv/app/
├── releases/20260805143000/ ← nova (só leitura para a aplicação)
├── releases/20260805120000/ ← anterior, intacta
├── shared/ ← sobrevive a todas as versões: logs, uploads, --shared-dir
└── current -> releases/20260805143000
/var/lib/sshepherd/secrets/app.env ← o cofre, root:root 600; a aplicação recebe pelo systemdDepois de publicar, o framework bate na porta da aplicação até 6 vezes. Se não responder, mostra o log e oferece o rollback como primeira ação. Voltar leva segundos, porque o código antigo já está no servidor.
O que ele não faz
- Não cria a VPS pela API do provedor — acompanha você no painel
- Não registra domínio
- Não administra banco de dados (backup, migração, tuning)
- Não roda migração de esquema, e o rollback não desfaz banco
- Não deixa processo rodando no servidor
- Não orquestra múltiplos servidores, nem Kubernetes
Estrutura instalada
seu-projeto/
├── .sshepherdignore o que NAO sobe para o servidor (editavel)
├── .sshepherd/
│ ├── state.json estado e estágio do onboarding
│ ├── config.toml preferências (editável)
│ ├── servers/<id>.json um arquivo por servidor
│ ├── logs/audit-*.jsonl histórico, um evento por linha
│ ├── scripts/sh|ps/ os scripts que executam de fato (secrets.sh, deploy.sh, ...)
│ └── bin/
│ ├── guard-hook.js recusa ssh cru, .env local e escrita nos scripts (Node puro)
│ └── version-notice.js aviso de nova versão (Node puro)
├── .claude/skills/sshp*/
└── .agents/skills/sshp*/servers/ e logs/ já vêm no .gitignore local: contêm endereço de servidor e histórico operacional.
Desenvolvimento
npm test # 108 testes, executor nativo do node, sem framework de teste
npm run bump:check # confere se package.json e os 10 agentes estão na mesma versão
npm run bump # 0.0.2 -> 0.0.3, atualiza os 11 arquivos de uma vezOs testes cobrem o detector das 13 engines, a estrutura instalada, preservação de dados em reinstalação, estado corrompido, os hooks (criação, merge, não-duplicação, settings inválido, ampliação do matcher antigo), o guard-hook (recusa de binário remoto cru, trecho escondido depois de operador ou dentro de substituição, caminho do framework só como palavra de comando, arquivo de segredo local, escrita nos scripts, evento ilegível), o manifesto SHA-256, created_files, o .sshepherdignore (criação, não-sobrescrita, recusa do deploy sem ele), os guard rails, o cofre (helper em modo de teste, bloco de instalação, redação, argumentos fechados, comando livre sem teclado) e a ausência de CRLF nos scripts.
Versionamento
Só o último número muda. 0.0.1 → 0.0.2 → 0.0.3 → ...
Sem major, sem minor. A versão aparece em package.json e no metadata de cada um dos 10 agentes — npm run bump mantém os onze em sincronia, e npm run bump:check acusa se algum ficou para trás.
Fluxo de release:
npm run bump # incrementa
# registre a mudança no CHANGELOG.md
git commit -am "v0.0.7: <o que mudou>"
git tag v0.0.7 && git push --follow-tagsStatus
Versão 0.0.8. A 0.0.6 foi executada de ponta a ponta contra um VPS real, do Dia Zero ao deploy. O cofre da 0.0.8 foi validado por teste (helper em modo de teste, bloco de instalação decodificado e conferido, hook, executor, redação, argumentos) e por duas rodadas de revisão adversarial com um segundo modelo; a parte que roda como root no servidor (usuário de serviço, unidade do systemd, sudoers, migração) ainda não foi exercitada contra máquina real. Use uma VPS descartável antes.
Validado por teste: instalador, comandos, os dois hooks, o guard-hook (ssh cru, trecho escondido depois de operador ou em substituição, caminho do framework só como palavra de comando, arquivo de segredo local, escrita nos scripts, evento ilegível), guard rails (bloqueio incondicional, redação de segredo, recusa sem confirmação, piso de risco que só sobe, confirmação de teclado no destrutivo em produção e no comando livre, sudoers sem curinga, trava de alvo do attack), o cofre, sintaxe e fim de linha dos scripts, 108 testes.
Do attack-surface.sh, a sondagem externa e a detecção de arquivo exposto foram exercitadas contra um servidor local montado para isso: portas, /.env, /.git/HEAD, listagem de diretório e banner de versão disparam corretamente. A parte que roda por SSH não foi validada contra máquina real.
Ainda é projeto jovem: um servidor por vez, quatro provedores cobertos em detalhe, e só Ubuntu 22.04/24.04 e Debian 11/12. Use uma VPS descartável antes de apontar para produção. Os únicos que só leem, e portanto não alteram nada, são o audit.sh e o attack-surface.sh.
Uma varredura de conformidade contra a spec, feita em 2026-08-28, reproduziu sete defeitos rodando o código. Cinco foram corrigidos na 0.0.7, cada um com o teste que falha sem a correção. Os dois que sobraram são estruturais e estão descritos acima, em "onde essa camada ainda não chega". O registro completo, com mais 28 achados de menor confiança, fica em _reversa_bugs/ no repositório e não vai no pacote do npm.
Licença
MIT — sandeco
