I built a terminal-native Little Snitch alternative for macOS — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogI built a terminal-native Little Snitch alternative for macOS
    Back to Blog
    I built a terminal-native Little Snitch alternative for macOS
    go

    I built a terminal-native Little Snitch alternative for macOS

    Nick Ciolpan March 24, 2026
    0 views

    CLI Snitch monitors every outbound connection from your Mac and lets you allow or deny them with real pfctl firewall rules — all from the terminal.

    --- title: I built a terminal-native Little Snitch alternative for macOS published: true description: CLI Snitch monitors every outbound connection from your Mac and lets you allow or deny them with real pfctl firewall rules — all from the terminal. tags: go, macos, security, opensource cover_image: --- I wanted to know what my Mac was doing behind my back. Every app phones home. Electron apps ping telemetry endpoints. Browsers hit trackers. Even system processes make connections you never asked for. Little Snitch shows you all of this beautifully — it's a proper, polished product and well worth the money. This is not a replacement for Little Snitch. This is what happens when you're curious about network monitoring and want to see how far you can get with Go, `lsof`, and `pfctl` in a terminal. Think of it as a learning project that accidentally became useful. **CLI Snitch** watches every outbound TCP and UDP connection, prompts you to allow or deny, and enforces your decisions with real macOS firewall rules — all from the terminal. ## What it looks like ```console $ sudo cli-snitch watch 🚨 New Outbound Connection Detected ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📱 Application: Electron Helper 🌐 Destination: telemetry.example.com:443 🔌 Protocol: TCP 🏷️ Host Info: Amazon Web Services (AWS) ? What would you like to do? ✅ Allow Once 🔁 Allow Always ❌ Deny Once > 🚫 Deny Always ❌ Electron Helper DENIED -> telemetry.example.com:443 🔥 Firewall rule: block out proto tcp from any to telemetry.example.com port 443 🛡️ Firewall rule applied ``` That last line is real. It's not a log message — it's an actual `pfctl` rule injected into the macOS packet filter. The connection is blocked at the kernel level. ## How it works The architecture is straightforward: 1. **Detect** — `lsof -i tcp -i udp -n` every 2 seconds (adaptive intervals) 2. **Match** — Check new connections against saved rules (case-insensitive, supports glob patterns) 3. **Prompt** — If no rule matches, ask the user via an interactive terminal prompt 4. **Enforce** — Deny decisions create `pfctl` anchor rules that survive the session 5. **Remember** — Decisions are saved as JSON rules and logged to a JSONL history file The interesting engineering problems were: ### Prompt serialization Multiple connections can arrive within the same 2-second scan. If two prompts hit stdin simultaneously, everything breaks. I solved this with a buffered channel queue: ```go type promptRequest struct { conn *monitor.Connection resultCh chan promptResult } func (cp *ConnectionPrompter) QueuePrompt(conn *Connection) (*UserDecision, error) { req := &promptRequest{ conn: conn, resultCh: make(chan promptResult, 1), } cp.promptQueue <- req res := <-req.resultCh return res.decision, res.err } ``` A single goroutine reads from the queue and calls the survey prompt — one at a time, no collisions. ### pfctl input sanitization When a user denies a connection, the host and port values end up in a pfctl command. If I naively concatenated those strings, a malformed hostname could inject commands. Every value is validated against regex before it touches pfctl: ```go var hostPattern = regexp.MustCompile(`^[a-zA-Z0-9._:\-]+$`) var portPattern = regexp.MustCompile(`^[0-9]+$`) ``` ### Connection deduplication `lsof` reports *all* active connections every scan, not just new ones. The monitor maintains a map of seen connections plus a TTL-based "recent cache" — so connections that get cleaned up from the main map but reappear within 5 minutes don't re-trigger prompts. ## What you get 14 commands: ```plaintext watch Real-time monitoring (the main event) list-rules See all your allow/deny rules edit-rule Modify a rule inline import-rules Load rules from JSON export-rules Back up rules to JSON history View connection log with filters firewall-status Check pfctl integration list-firewall Show active blocking rules clear-firewall Remove all pfctl rules firewall-cleanup Remove expired temp rules firewall-monitor Live firewall status system-status Full diagnostics daemon install Set up as a launchd service daemon start/stop Control the background service ``` Some features I'm particularly happy with: - **Wildcard rules** — `*.analytics.com` blocks all analytics subdomains - **DNS reverse lookup** with caching — so you see hostnames, not just IPs - **Connection history** — every decision logged to JSONL, filterable by process or action - **Daemon mode** — `sudo cli-snitch daemon install` creates a launchd plist for background monitoring - **Rule scoping** — block a specific connection, all connections to a host, all connections on a port, or everything from a process ## Install ```bash brew tap nickciolpan/tap brew install cli-snitch sudo cli-snitch watch ``` Or build from source: ```bash git clone https://github.com/nickciolpan/snitcher cd snitcher go build -o cli-snitch ./cmd/cli-snitch sudo ./cli-snitch watch ``` ## What it can't do Being honest about the limitations: - **No per-process blocking in pfctl** — macOS packet filter doesn't have a concept of process ownership. If two apps connect to the same host:port, a deny rule blocks both. True per-process filtering needs Apple's Network Extension framework (Swift, not Go). - **No bandwidth tracking** — would need BPF packet capture. - **lsof truncates process names** — `Google Chrome Helper` becomes `Google`, `Slack` becomes `Slack\x20`. Works fine once you know the quirk. ## What I learned 1. **lsof is surprisingly good** for this use case. It's fast, available everywhere, and the output is parseable. The main gotcha is IPv6 bracket notation and process name encoding. 2. **pfctl anchors are the right abstraction.** By isolating all CLI Snitch rules in a named anchor, there's zero risk of clobbering system firewall rules. Cleanup is just "reload an empty anchor file." 3. **Interactive CLI tools need careful goroutine design.** The prompt queue pattern — buffered channel + single-reader goroutine — is something I'll reuse in every interactive CLI from now on. --- The source is at [github.com/nickciolpan/snitcher](https://github.com/nickciolpan/snitcher) and the docs are at [cli-snitch.ciolpan.com](https://cli-snitch.ciolpan.com). MIT licensed. If you've ever wondered what your Mac is doing when you're not looking, give it a try. You might be surprised. --- *Yes, this was written with the help of an LLM. The code too. Are we still pretending that's not how things get built in 2026? Claude wrote most of the implementation, I steered, tested, broke things, and made the decisions. The architecture is real, the bugs were real, and the pfctl rules definitely blocked my Chrome tabs for real. Tools are tools — what matters is whether the thing works. It does.*

    Tags

    gomacossecurityopensource

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.