# Arquivos de inicialização do SSH: por que os comandos remotos enganam os agentes

Um comando SSH não é automaticamente o programa que você digitou. Antes que a máquina remota execute `git`, `python` ou um script de implantação, o daemon SSH seleciona uma conta, inicia o shell dessa conta e entrega a ele uma sequência de comandos. Arquivos de inicialização e regras do servidor podem alterar o ambiente, substituir o comando ou tornar uma execução interativa enquanto outra permanece não interativa.

Isso importa ainda mais quando um agente relata o resultado. Uma pessoa que vê um banner inesperado, um alias ou um prompt colorido pode parar e investigar. Um agente pode interpretar o código de saída 0 e uma linha de saída com aparência familiar como prova de que o programa esperado foi executado. Já vi essa suposição transformar uma verificação de status inofensiva em uma chamada para um script wrapper, e uma verificação de implantação em um resultado vindo do executável errado.

A solução não é apagar todos os arquivos de perfil. As pessoas precisam de shells interativos utilizáveis. O caminho é identificar o fluxo de execução, separar a conveniência humana da automação e deixar explícito o contrato do comando remoto.

## Um comando remoto passa primeiro pelo shell da conta

O OpenSSH normalmente não executa as palavras depois de `ssh host` como um `execve` direto do binário de destino. O manual do `sshd` diz que, depois da autenticação, o sshd executa o comando solicitado por meio do shell do usuário, usando a opção `-c` do shell. O caminho do shell de login vem do banco de dados da conta, não da máquina local que abriu a conexão SSH.

Esse detalhe explica muitos relatos confusos. Suponha que um agente envie:

```text
ssh deploy@buildbox git -C /srv/app rev-parse HEAD
```

A conta remota pode usar Bash, zsh, fish, um shell restrito ou um wrapper do ambiente. O shell selecionado recebe um comando equivalente a `git -C /srv/app rev-parse HEAD`. Primeiro, ele pode se inicializar. Também pode receber um comando substituto do sshd antes de chegar a esse ponto.

Isso é separado do shell que o agente usou localmente. Um agente pode ser executado em um processo limpo em um Mac, enquanto a conta remota `deploy` tem dez anos de personalizações pessoais de shell. Um prompt local limpo não torna o outro lado limpo.

Também não confunda a análise de comandos do shell com o transporte SSH. O SSH criptografa a conexão e autentica a conta. Ele não promete que `python` significa o binário esperado, que o PATH permaneceu inalterado ou que um perfil não imprimiu texto na saída padrão.

A primeira pergunta prática, portanto, não é «O SSH se conectou?». É «Qual programa interpretou o comando remoto, sob qual conta e com quais regras de inicialização?»

## O modo do shell decide quais arquivos participam

O comportamento de inicialização depende da família do shell e do modo de invocação. Os rótulos usados casualmente, «um shell SSH» ou «um shell Bash», não são precisos o bastante para prever o comportamento.

No Bash, o GNU Bash Reference Manual distingue a invocação de login, interativa e não interativa. Um shell Bash de login lê `/etc/profile` e depois o primeiro arquivo pessoal legível entre `~/.bash_profile`, `~/.bash_login` e `~/.profile`. Um shell interativo que não é de login lê `~/.bashrc`.

Um comando remoto comum geralmente é executado em um shell não interativo. Isso não significa que ele não carregue nada. O Bash lê o arquivo indicado por `BASH_ENV`, se essa variável estiver definida, antes de executar um comando não interativo. O manual do Bash também documenta um tratamento especial quando o Bash detecta que o sshd o iniciou com a entrada padrão conectada a uma conexão de rede: ele pode ler `~/.bashrc`. Não baseie a automação na regra informal de que `.bashrc` afeta apenas o uso interativo.

O zsh tem um mapa diferente. `/etc/zshenv` e `~/.zshenv` se aplicam a todas as invocações do zsh, o que torna um `.zshenv` barulhento ou cheio de efeitos colaterais especialmente perigoso. O zsh lê `.zprofile` para shells de login e `.zshrc` para shells interativos. Quem coloca alterações de PATH e mensagens de status em `.zshenv` modifica comandos remotos, scripts e muitas vezes também a inicialização de aplicativos gráficos.

O `sh` POSIX não oferece uma saída universal. As implementações diferem. A especificação do shell POSIX descreve o arquivo `ENV` para shells interativos, mas o `/bin/sh` de um sistema pode ser dash, Bash em modo de compatibilidade ou outra implementação com detalhes próprios. Teste o host real em vez de presumir que um nome de arquivo significa a mesma coisa em todos os lugares.

Essa distinção tem uma consequência operacional importante. Um comando que funciona em uma sessão interativa de diagnóstico pode falhar para um agente porque a sessão interativa lê um perfil que a sessão de comandos ignora. O inverso também acontece: um hook de automação pode afetar as sessões de comandos enquanto o terminal de uma pessoa não o percebe.

Mantenha as configurações interativas em arquivos lidos apenas por shells interativos. Prompts, configuração de conclusão, títulos de terminal, padrões de cores e saudações pertencem a esses arquivos. Coloque os requisitos de ambiente da automação em um arquivo pequeno e documentado, que um launcher controlado carregue deliberadamente. Não deixe que um agente descubra seu ambiente de execução herdando os dotfiles de um desenvolvedor.

## Alterações no PATH mudam a identidade, não apenas a conveniência

O PATH costuma ser tratado como uma configuração de conveniência. Em execuções sem supervisão, ele seleciona a identidade do programa. Se um perfil coloca `/opt/team/bin` no início, `curl`, `git`, `ssh` ou `python` pode apontar para um wrapper em vez do programa do sistema.

Esse wrapper pode ser intencional. Equipes usam wrappers para escolher credenciais de nuvem, impor verificações de repositório, adicionar telemetria ou selecionar runtimes de linguagem. O erro é permitir que um agente presuma que um comando sem caminho qualificado significa o executável do fornecedor. Ele significa apenas «o primeiro executável correspondente no PATH que este shell tem neste momento».

Aliases e funções criam uma ambiguidade semelhante, embora seu comportamento dependa do modo do shell. O Bash normalmente não expande aliases em um shell não interativo, a menos que `expand_aliases` esteja habilitado. Isso torna os aliases menos comuns em execuções padrão de comandos SSH, mas não impossíveis. Um arquivo carregado pode habilitar a expansão. Funções não precisam da expansão de aliases. Um perfil pode definir uma função chamada `git`, exportar o estado do ambiente e fazer com que todos os comandos seguintes naquele shell chamem a função.

Comece perguntando ao shell, mas trate a resposta como evidência do ambiente que está sendo inspecionado, não como uma prova universal:

```sh
ssh -T deploy@buildbox '
printf "shell argv0: %s\n" "$0"
printf "shell flags: %s\n" "$-"
printf "PATH: %s\n" "$PATH"
command -V git
command -V python
command -V curl
'
```

Um resultado típico pode ser:

```text
shell argv0: -bash
shell flags: hBc
PATH: /opt/team/bin:/usr/local/bin:/usr/bin:/bin
git is /opt/team/bin/git
python is /usr/local/bin/python
curl is /usr/bin/curl
```

O builtin `command -V` pode informar se o nome é um alias, uma função, um builtin ou um caminho. Nos shells que oferecem esse recurso, `type -a git` pode exibir mais de um caminho correspondente. No Bash, `declare -f git` imprime o corpo de uma função se `git` for o nome de uma função. Essas verificações revelam uma falha comum: um perfil define uma função `git()` que executa um comando Git real e depois envia discretamente um evento de status. A saída continua parecendo a saída do Git. O efeito colateral acontece em outro lugar.

O armazenamento em cache de comandos acrescenta outra complicação. Alguns shells armazenam o local onde encontraram um comando. Se um perfil altera o PATH depois que o shell resolveu um nome, o shell pode continuar usando o local armazenado até que `hash -r`, ou o equivalente, limpe o cache. Isso aparece sobretudo em shells interativos de longa duração, mas agentes que mantêm um processo de shell ativo também podem sofrer com isso.

Use caminhos absolutos para comandos cuja identidade afeta uma decisão de segurança ou um resultado de implantação. Não escreva `/usr/bin/git` apenas porque ele existe no seu laptop. Confirme o caminho esperado no sistema operacional de destino, registre-o e teste-o com a conta que executará o trabalho. Se a implantação usa deliberadamente um gerenciador de versões ou wrapper, nomeie esse wrapper de forma explícita e faça do contrato dele parte do job.

## Um pseudo-terminal muda mais do que a formatação

`ssh host command` normalmente não aloca um pseudo-terminal. `ssh -t host command` força um. Essa única opção pode mudar os caminhos de inicialização do shell, afetar o buffer de saída, fazer programas emitirem códigos de cor e solicitar uma entrada que uma execução sem TTY nunca pediria.

Muitos arquivos de configuração do shell começam com um teste como este:

```sh
case $- in
  *i*) ;;
  *) return ;;
esac
```

Essa verificação encerra o restante do arquivo, a menos que o shell se identifique como interativo. Um pseudo-terminal pode tornar um shell interativo em circunstâncias nas quais uma conexão de comando comum não seria. O comando passa então a receber aliases, configuração de conclusão, auxiliares de prompt, alterações no PATH e exportações específicas do terminal que não existiam na execução do agente.

Os programas também reagem diretamente ao terminal. Um comando pode exibir uma barra de progresso, paginar a saída, escolher outro formato de diagnóstico ou ler uma confirmação. Um parser de máquina que espera um único objeto JSON pode falhar porque um perfil imprime uma saudação antes do início do programa ou porque o programa detecta um TTY e emite caracteres de controle.

Teste os dois modos ao investigar uma diferença:

```sh
ssh -T deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
ssh -tt deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
```

O primeiro comando pede ao SSH que não aloque um terminal. O segundo força um, mesmo quando a entrada padrão local não é um terminal. Compare a saída e depois compare a resolução dos comandos e o PATH em cada modo. Se os resultados forem diferentes, não corrija primeiro o parser. Encontre o caminho de inicialização que causou a diferença.

A automação deve usar `-T` por padrão. Aloque um terminal apenas para um comando que realmente precise dele, como uma tarefa controlada de recuperação interativa. Se um agente precisa de um terminal para executar uma consulta de status rotineira, ele já está operando em um ambiente menos previsível.

## As regras do servidor podem substituir o comando solicitado

Os arquivos de inicialização são apenas uma camada. O servidor pode substituir ou restringir um comando antes que o shell da conta o veja. Se você inspecionar `.bashrc` e parar por aí, pode deixar passar a regra que realmente mudou o resultado.

O manual de `sshd_config` documenta `ForceCommand`. Um administrador pode aplicá-lo globalmente ou dentro de um bloco `Match` para um usuário, grupo, endereço ou outra condição. Com `ForceCommand internal-sftp`, por exemplo, o servidor ignora comandos comuns do shell e executa o serviço SFTP interno. Com um wrapper personalizado, o wrapper recebe o contexto do comando solicitado e decide o que fazer.

Uma entrada de chave pública SSH também pode incluir uma restrição `command="..."` no arquivo `authorized_keys` da conta. Isso é comum em contas de backup, acesso a repositórios, transferência de arquivos e automação com escopo limitado. Pode ser uma boa prática de segurança. Torna-se um problema de diagnóstico quando alguém fornece uma credencial restrita a um agente e espera execução arbitrária de comandos.

Os controles de ambiente também precisam ser revisados. `AcceptEnv` informa ao sshd quais variáveis enviadas pelo cliente ele aceita. `SetEnv` pode definir variáveis no servidor. `PermitUserEnvironment`, quando habilitado, pode permitir que o arquivo de ambiente SSH da conta ou as opções da chave autorizada definam valores. Essas configurações podem influenciar o PATH, a localidade, o comportamento do runtime de uma linguagem e hooks como `BASH_ENV`.

Use `ssh -G` no cliente, mas conheça seu limite:

```sh
ssh -G buildbox | grep -E '^(hostname|user|port|requesttty|remotecommand|sendenv|setenv) '
```

O OpenSSH exibe a configuração do cliente depois de aplicar os blocos `Host` locais e os valores padrão. A saída pode revelar um `RemoteCommand` inesperado, uma configuração de terminal forçada, uma regra de encaminhamento de ambiente ou outro host de destino. Ela não revela `ForceCommand` do servidor, o shell da conta remota nem as restrições armazenadas em `authorized_keys`.

Peça ao responsável pelo servidor o `sshd_config` relevante e as restrições da conta quando a conta for destinada à automação. Se você não administra o host, peça uma interface de execução documentada em vez de tentar reconstruir o comportamento de um shell pessoal por meio de execuções de teste. Uma conta com um wrapper forçado opaco não é um endpoint SSH genérico, mesmo que aceite autenticação.

## Inspecione o fluxo de execução sem confiar em uma única verificação

Um comando de diagnóstico é executado exatamente no ambiente sob suspeita. Isso não torna o diagnóstico impossível. Significa que você deve coletar vários fatos independentes e indicar o que cada um comprova.

Primeiro, inspecione a configuração do cliente local com `ssh -G`. Depois, determine o shell configurado para a conta remota por meio do banco de dados de contas do host. Em muitos hosts Linux, `getent passwd deploy` retorna um registro separado por dois-pontos cujo último campo é o caminho do shell. No macOS, um administrador pode inspecionar a conta com ferramentas do Directory Service. Quando possível, faça isso por um canal administrativo confiável, em vez de usar o shell potencialmente personalizado da conta.

Em seguida, obtenha e revise os arquivos de inicialização do shell real: arquivos globais, arquivos pessoais e todos os arquivos que eles carregam. Procure estes tipos de comportamento:

- atribuições a `PATH=`, inicialização de gerenciadores de versões e comandos `hash`
- `alias`, definições de funções e opções do shell, como `expand_aliases`
- comandos de saída, como `echo`, `printf` e auxiliares de controle do terminal
- `BASH_ENV`, `ENV`, variáveis de localidade e hooks de ambiente específicos de linguagens
- ramificações condicionais que testam `-t`, `$-`, `$SSH_TTY` ou `$TERM`

Não procure apenas pela palavra `alias`. Uma linha como `. ~/.local/share/tool/init.sh` pode carregar o arquivo que define a função importante para você. Siga a cadeia de arquivos carregados até o fim. Arquivos de perfil frequentemente se transformam em uma pilha de includes condicionais, e é aí que o comportamento da automação deixa de ser revisável.

Depois, faça uma coleta de impressões digitais limitada, usando um comando inofensivo. Registre as flags do shell, o diretório atual, o PATH, os nomes de ambiente relevantes e a resolução exata das ferramentas que o job executará. Evite despejar todas as variáveis de ambiente nos logs. Tokens e credenciais de nuvem frequentemente ficam ali. Uma transcrição de diagnóstico deve comprovar as condições de execução sem se transformar em um novo depósito de segredos.

Por fim, teste o comando exato de produção no mesmo modo que o agente usará: mesma conta, sem terminal, a menos que seja necessário, mesmo transporte de entrada e mesmo diretório de trabalho. Um resultado obtido por meio do login interativo de um administrador não é equivalente. Ele responde a outra pergunta.

Mantenha as evidências junto da definição da implantação ou da automação. O artefato útil não é uma captura de tela de um terminal funcionando. É um documento curto que nomeia o shell da conta, os arquivos de inicialização autorizados a afetar a execução, os caminhos exigidos dos executáveis, o ambiente esperado, a política de terminal e as restrições de comandos do servidor.

## Torne o shell da automação deliberadamente monótono

Uma ação remota confiável deve entrar em um shell conhecido com um ambiente curto e então executar binários nomeados. Isso não remove o shell inicial da conta usado pelo sshd, mas limita quanto do estado herdado chega ao programa que realiza o trabalho.

Para um script pequeno e fixo, envie o script pela entrada padrão e execute um shell conhecido com um ambiente vazio. A sequência de comandos remotos permanece fixa, o que também evita construir uma torre frágil de escapes de aspas locais e remotas.

```sh
ssh -T deploy@buildbox '/usr/bin/env -i PATH=/usr/bin:/bin /bin/sh -s' \u003c\u003c'REMOTE'
set -eu
PATH=/usr/bin:/bin
export PATH

/usr/bin/git -C /srv/app rev-parse HEAD
REMOTE
```

Isso oferece propriedades úteis. `-T` evita um terminal. `/usr/bin/env -i` limpa as variáveis herdadas. `/bin/sh -s` lê o script literal da entrada padrão, e o heredoc entre aspas impede que o shell local expanda `$PATH` ou outro texto antes da transmissão. `set -eu` interrompe a execução diante de um parâmetro não definido ou de um comando simples que falhe, respeitando a semântica normal do shell.

Também há limites. `/usr/bin/env`, `/bin/sh` e `/usr/bin/git` são exemplos, não caminhos que devam ser copiados sem verificação para todos os hosts. Confirme os caminhos em cada classe de destino. Limpar o ambiente pode remover variáveis de que um programa realmente precisa, incluindo o diretório inicial, a localidade, a configuração de proxy ou o local do runtime. Adicione apenas as variáveis nomeadas de que o programa necessita e documente o motivo de cada uma.

Não use `env -i` para esconder uma configuração de conta quebrada. Se um arquivo de inicialização altera o shell inicial de forma tão agressiva que impede a execução do launcher fixo, a própria conta não é adequada para trabalho sem supervisão. Use uma conta dedicada, com shell de login controlado e poucos arquivos de inicialização.

Evite inserir texto arbitrário do agente em uma sequência remota `sh -c`. Erros de aspas podem transformar dados em sintaxe do shell antes que o script pretendido os receba. Passe os dados por um canal restrito, use um script remoto fixo com argumentos validados ou utilize um protocolo criado para solicitações estruturadas. O shell é poderoso porque interpreta texto como código. Um agente não deve poder apagar essa fronteira por acidente.

## Separe contas interativas de contas de agentes

O design mais limpo dá identidades remotas diferentes para pessoas e automação. Uma conta de desenvolvedor pode manter seu prompt, a conclusão de comandos, gerenciadores de linguagens e auxiliares pessoais. Uma conta de automação deve ter um shell declarado, um diretório inicial pequeno e arquivos de inicialização que não façam nada em sessões de comandos ou executem uma configuração estreitamente revisada.

Essa separação não é burocracia. Ela permite responder a perguntas simples, mas necessárias: Qual binário do Git faz a implantação? Qual é o diretório inicial? A conta aceita um terminal? Quais nomes de ambiente podem afetar uma versão? Quem pode alterar o wrapper que executa os comandos?

Uma conta dedicada também facilita o raciocínio sobre restrições de comandos. Você pode usar um wrapper forçado para as poucas ações que a conta deve executar e rejeitar todo o restante. Isso não transforma o wrapper em uma linguagem de políticas. Mantenha-o simples o bastante para ser inspecionado: valide os argumentos, defina o ambiente documentado, invoque um caminho absoluto de programa e escreva um registro de auditoria.

Não tente resolver isso colocando dezenas de exceções em um grande `case` dentro do `.bashrc`. Essa abordagem é popular porque todo usuário já tem um arquivo de perfil e nenhuma alteração de implantação é necessária. Ela falha porque a semântica do perfil varia conforme o modo do shell, a ordem dos arquivos carregados fica obscura e uma edição interativa inofensiva pode alterar o comportamento sem supervisão. Coloque a configuração da automação no launcher ou no script dedicado que administra o contrato da automação.

O Sallyport pode manter as credenciais SSH fora do processo do agente e executar ações SSH por meio do helper `sp-ssh` integrado, mas não pode tornar determinística uma conta remota sem controle. Trate a revisão da conta remota como parte do design da ação e use o registro de atividades para comparar o que o agente solicitou com o que o lado remoto retornou.

## Confie nos resultados apenas depois de definir o contrato

O resultado de um comando de agente é confiável quando você consegue declarar o que foi executado sem depender dos hábitos atuais do shell de uma pessoa. Essa declaração precisa de mais do que um hostname e um código de saída. Ela deve identificar a conta SSH, a identidade do host de destino, a configuração do terminal, a restrição de comando do servidor, se houver, o shell selecionado, a construção do ambiente, a origem do script e o caminho do executável.

Existe um limite incômodo aqui. Você não pode provar, a partir da própria saída de um comando remoto, que o shell remoto não alterou o comando antes de imprimir aquela saída. Se o shell da conta, a restrição da chave autorizada ou o comando forçado estiver fora do seu controle, você precisa de evidências administrativas ou de outra conta. Repetir a mesma verificação dez vezes apenas repete a mesma suposição.

Comece pelos comandos que os agentes já executam e que podem alterar código, infraestrutura ou dados de produção. Para cada um, execute-o com e sem terminal, inspecione como o executável é resolvido e substitua a configuração herdada por um launcher explícito quando esse resultado for relevante. A primeira surpresa geralmente é o PATH. A segunda costuma ser um arquivo de inicialização que alguém esqueceu que ainda era carregado.

Depois de encontrar uma dessas surpresas, não adicione outra condicional ao perfil e considere o problema resolvido. Mova o comportamento para uma conta controlada ou um script fixo, registre o contrato do comando e torne a próxima execução do agente tão previsível que a saída signifique exatamente o que diz.
