エンジニアリング

ShellMon の作り方:危険なシェルコマンドをリアルタイム検出

ShellMon は SSH セッションを監視し、rm -rf / のキー入力がサーバーに届く前にブロックします。ターミナルを遅くせずにどうやったか。

CC Chen Chen· 創業者·2026年6月1日·12分で読了

課題

モバイル SSH セッションにユーザーが rm -rf / を貼り付けるのは、デスクトップと同じくらい簡単です。むしろもっと簡単かもしれません ── オートコレクト、AI の提案、太い指でのタップ。キーストロークがサーバーに届く**前**に動作する遮断層が必要でした。

素朴な答えは、危険な文字列のブロックリストです。しかしシェルは言語であって、固定のフレーズ集ではありません。rm -rf /rm -fr /rm --recursive --force /r''m -rf / は同じ意図を別の衣装で身につけているに過ぎません。文字どおりのテキストだけをマッチさせる方法は、最初に引用符を加える人によって破られます。

検討した3つのアプローチ

  1. クライアント側の正規表現。送信前に危険なパターンをマッチさせる。速いが、簡単に回避される ── r''m -rf / は単純なマッチを潜り抜けます。
  2. サーバー側のラッパー。すべてのサーバーにラッパースクリプトをインストール。信頼できるが、すべてのホストを変更する必要がある ── 事前設定していないサーバーに触れることが要のモバイル中心クライアントには、これは始められません。
  3. ハイブリッド:クライアント側パース + 正規化。コマンドをトークン化、正規化し、厳選したルールセットに対してマッチさせる。これが私たちが出したものです。

検出パイプライン

検出はデバイス上で3段階で実行されます:正規化、トークン化、マッチング。仕事のほとんどは正規化器にあります ── 引用符を展開し、明らかなエスケープを解決し、コマンド区切りで分割して各セグメントを独自に判断します。

def is_dangerous(command: str) -> tuple[bool, str | None]:
    tokens = shlex.split(canonicalize(command))
    for rule in DANGEROUS_RULES:
        if rule.matches(tokens):
            return True, rule.reason
    return False, None

canonicalize() はマッチした引用符ペア(r''mrm)を削除し、引用符外のバックスラッシュエスケープを折り畳み、既知の安全な環境変数展開の小さなセットを置換し、&&;| で複合コマンドを分割するため、危険なセグメントがパイプライン中に隠れることができません。各セグメントは独立してルールセットを通ります。ルールはコードではなくデータです ── トークンパターンと人間が読める理由文字列を持つ YAML パックで、ブロックされたときにユーザーに表示されます。

なぜここではパターンマッチングが LLM に勝ったか

LLM で危険コマンドを分類することも検討しました。動作はする ── しかし遅い(キーストロークあたり 300〜800ms の遅延)、その呼び出しボリュームでは高価、過度に慎重:find . -delete を「delete」が危険そうに見えるからブロックします。

手調整のルールによるパターンマッチングは、サブミリ秒のレイテンシ、完全オフライン、監査可能 ── コマンドがフラグされた正確な理由を読めます。だから仕事を分けました:パターンマッチングが**検出**、LLM が**説明**。コマンドがフラグされた後、AI ヘルパーはなぜかを平易な言葉で説明できますが、すべてのキーストロークのホットパスには絶対に座りません。

Y/n 自動応答

ShellMon が解決するもう一つの別問題:apt upgradeDo you want to continue? [Y/n] を尋ねますが、ユーザーは画面を点けたままにせずに応答されたい。難しいのは Y をタイプすることではありません ── セッションが**入力待ち**なのか、まだ作業中なのかを知ることです。

# 起動する各コマンドに付加されるセンチネル
cmd; __ec=$?; printf '\n__TERMAI_END_%s__%d__\n' "<hex>" "$__ec"

センチネルは二重の役目を果たします。出力ストリームに現れたら、コマンドが終了したと分かり、プロンプトを解析せずに終了コードを取得できます。それが**現れず**、出力が既知のプロンプトパターン([Y/n](yes/no))で終わる行で静かになったら、セッションが入力でブロックされていると確信できます ── そのときだけ自動応答ルールが発火します。ランダム化された16進数により、たまたま「END」を含むプログラム出力に対してマーカーが衝突しないようになっています。

検出できないもの

正直な制限:
  • カスタムバイナリ。./my-script.sh が何をするかを検査できません。呼び出しのみをチェックし、内容はチェックしません。
  • パイプチェーン。grep/awk/jq をチェーン中に挟む長いパイプラインは、危険なパターンを通すことができます。明白なケースはブロックしますが、すべての敵対的な例をキャッチするわけではありません。
  • 意図的な誤用。サーバーをワイプしたい**意図的**なユーザーは、方法を見つけます。ShellMon はセーフティネットであり、アクセスコントロールではありません。

リリースしたもの、次に行うもの

ShellMon は TermAI v0.9 でリリースされました。それ以来、カスタマイズ可能なルールパック、長時間ジョブ完了時の Pro 階層プッシュ/メール通知、コマンド説明のための AI ヘルパー統合を追加しました。

次:スニペット対応モード ── 自分のライブラリから検証済みのスニペットを実行している場合、ShellMon はより信頼し、より静かになります。そしてユーザーが監査し、見逃したパターンを貢献できるよう、ルールセットを GitHub で公開します。

Try TermAI

Free on iOS and Android. 3 SSH connections + 20 AI calls/day on the free tier.

CC
Chen Chen — Founder of TermAI

Writes about mobile DevOps, terminal UX, and the surprising depth of "boring" infrastructure.

💬 Discuss this article: Hacker News · Reddit · V2EX
Was this useful? ← Back to blog