Charon is a Go SSH proxy. It accepts client SSH connections, authenticates users, opens one configured upstream SSH connection, forwards channels/requests, records client stdin, extracts commands, stores session metadata in
SQLite, then optionally queues LLM analysis through a separate analyzer process.

The core architectural pitch: the SSH data path stays lean, and expensive/flaky model work is moved to a durable SQLite queue consumed by charon-analyzer.

Entrypoints

  • Proxy binary: cmd/charon/main.go:17
    Loads config, opens SQLite store, builds proxy.Server, starts listening.

  • Analyzer binary: cmd/charon-analyzer/main.go:30
    Loads analyzer config, checks API key/analysis enabled, starts N workers.

  • Config shape/defaults: internal/config/config.go:155
    TOML config, strict unknown-key validation, separate proxy/analyzer validation.

Proxy Path

Main file: internal/proxy/server.go:30

Flow inside handleTCPConn: internal/proxy/server.go:100

  1. SSH handshake with client.
  2. Open per-session raw stdin log.
  3. Create session row in SQLite.
  4. Dial upstream SSH.
  5. Bridge global requests and channels both ways.
  6. On close, finalize session: parse commands, apply rules, persist facts, enqueue analysis.

Forwarding/channel logic lives in internal/proxy/bridge.go:91. It forwards session channels, exec/shell/PTY-ish request traffic, stderr, exit status, and optionally TCP/agent/X11 depending on config. Client-to-upstream stdin is
tee’d into the raw log.

Auth lives in internal/proxy/auth.go:28. Nice detail to mention: password auth always does one bcrypt comparison, using a dummy hash for unknown users, to avoid username timing leaks.

Session Logging And Command Extraction

  • Opens session log dirs/files: internal/commandlog/session.go:23
  • Extracts commands from raw terminal input: internal/commandlog/normalize.go:48

This is not a dumb line split. It replays terminal editing semantics: backspace, Ctrl-C, Ctrl-U, ANSI escape skipping, limits for hostile giant input, etc. That’s probably one of the more interesting implementation areas.

Artifacts are shaped like:

<log_dir>/<safe_username>/_<session_id>/stdin.raw
stdin.commands.jsonl
stdin.llm.summary.json
stdin.requester.llm.summary.json

Finalize / Analysis Queue

Session finalization is here: internal/proxy/finalize.go:20

It:

  • Reads stdin.raw
  • Extracts commands
  • Redacts sensitive values
  • Applies static analyzer rules
  • Writes stdin.commands.jsonl
  • Persists commands/session risk to SQLite
  • Enqueues an LLM analysis job if analysis is enabled

Queue code: internal/analyzer/jobs.go:18

It uses durable SQLite rows with statuses like queued/running/failed/dead/succeeded, leases, retry backoff, and max attempts.

Analyzer Worker

Worker loop: internal/analyzer/worker.go:14

Important method: internal/analyzer/worker.go:65

The worker claims a job, prepares model choice based on scope, calls an OpenAI-compatible API, saves results, maybe queues a cross-session requester investigation, then completes the job.

OpenAI-compatible client: internal/analyzer/openai_compatible.go:1

The code asks for JSON output and validates the model result shape before saving.

Correlation

Cross-session/requester correlation: internal/analyzer/correlation.go:21

It joins sessions by username or public key fingerprint within a lookback window. If a session’s risk meets the configured threshold and there are related summaries, it queues a requester investigation job.

Storage

SQLite store setup: internal/store/store.go:22

Generated query layer comes from sqlc. Queries are in internal/store/queries.sql:1. Migrations are embedded from internal/store/migrations.

A couple patterns worth saying out loud:

  • modernc.org/sqlite, so pure Go SQLite.
  • WAL mode for file-backed DBs.
  • busy_timeout.
  • SetMaxOpenConns(1), conservative because SQLite has one writer anyway.

Tests

Tests are mostly real-ish integration/unit paths, not mock soup:

  • SSH auth/bridge/server tests under internal/proxy
  • Command parser tests under internal/commandlog
  • Queue/correlation/OpenAI-compatible tests under internal/analyzer
  • Store migration/query tests under internal/store

Useful command:

go test ./…

README.md says the normal local build is:

go test ./…
go build ./cmd/charon
go build ./cmd/charon-analyzer

Good Interview Sound Bites

  • “The proxy never calls the LLM provider directly. It only writes durable analysis jobs, so slow model calls cannot stall SSH sessions.”
  • “The bridge uses x/crypto/ssh directly because the code needs raw channel/request behavior.”
  • “Raw stdin is treated as hostile input. Command extraction streams, enforces caps, and models terminal editing instead of regexing the whole file.”
  • “SQLite is both the session index and durable job queue. Analyzer workers are stateless consumers with leases and retry backoff.”
  • “Correlation is intentionally identity-based across username and public key fingerprint, not just session-local.”