At one point during development, NetBlocker broke the chat integration in Android Studio.
The failure looked unrelated to a network filter. Android Studio reported a bad file descriptor, its embedded tooling stopped working, and restarting it made no difference. The actual cause was an early NetBlocker rule derived from Adobe Photoshop.
Photoshop includes several auxiliary processes. One of the identities discovered inside its bundle was node. Android Studio also runs a Node process. NetBlocker had reduced the identity too aggressively, concluded that the Android Studio process belonged to Photoshop, and blocked it.
The rule was internally consistent. It was also wrong.
That bug captures most of the difficulty in building NetBlocker. The user asks to block an app. macOS gives the filter processes, signed code and network flows. Turning one into the other without catching unrelated software is the real job.
An app is a user concept
In Finder, an app is one icon. Internally, it is usually a directory containing a main executable and an assortment of other components. There may be helpers, login items, app extensions, update tools, separate interprocess services and embedded command-line programs. Any of them can open a connection.
Blocking only the main executable is therefore easy to implement and easy to misunderstand. The interface can show Photoshop as blocked while an updater or helper continues to communicate. That may satisfy the literal rule in the code, but it does not satisfy what the person using the product meant.
I wanted the unit of control in NetBlocker to remain the visible app. You drag an app into the window. NetBlocker finds relevant components, keeps them grouped under that app and applies the rule when those components create new outbound connections.
The distinction is important because the grouping is not only visual. It defines the scope of the rule.
Why the process name is almost useless
The Photoshop incident came from treating a weak identifier as if it were strong. Executable names are labels, not identities.
Generic runtimes make this obvious. node, java, python, bash and sh may appear inside many unrelated products. A rule that follows one of those names globally will eventually hit something the user never selected. Numeric process identifiers are temporary and get reused. File paths are more informative, but they change when software is moved or updated.
macOS code signing gives us better material. Apple's Code Signing Services can describe the signing identity, developer team and requirements attached to code. Those values are useful because they say more about the origin of an executable than its filename does.
They still do not form a magic app identity. A signing identifier can be generic. A developer team can own hundreds of unrelated binaries. A bundle path is useful context, but only while the component actually belongs under that bundle.
NetBlocker now evaluates identity using several pieces of evidence together: the signed identity of the running code, the team behind it, its relationship with the selected bundle and the process context attached to the network flow. I deliberately avoid turning a generic identifier into a machine-wide rule.
That is the fix for the Android Studio bug. node is no longer enough to say “this belongs to Photoshop”.
Inspection tells you what might run
Dragging an app into NetBlocker triggers an inspection of its bundle. Signed embedded components provide a useful initial map, so the product can show related processes before every feature of the target app has been exercised.
This works well for software that ships as a mostly self-contained bundle. It is only a starting point.
An app can install or update a helper later. Some components appear only after sign-in or after a particular feature is used. A generic runtime can load code whose relationship to the parent app is not obvious from the runtime's own name. Updates can replace internal components while leaving the outer bundle identifier unchanged.
Runtime evidence fills in part of that picture. The Network Extension Filter Data Provider receives a flow when a process creates a network connection. NetBlocker can inspect the source process context, compare it with the current rules and record a related process when the evidence is strong enough.
There is a boundary I do not want to cross: observing a process once should not silently grant it permanent membership in an app group. Discovery is evidence, not authority. Otherwise a temporary coincidence can become a lasting false positive.
The filter cannot behave like a normal app
Network decisions sit on a path that applications expect to be fast. If the filter pauses to query a server, scan a bundle or perform heavy storage work, the delay is experienced by the app opening the connection.
The filter also runs under restrictions appropriate for code that can inspect device-wide network flows. This is not the place for most product logic.
NetBlocker prepares policy outside the decision callback and gives the filter a compact local representation. Identity work is bounded and cached. Activity updates are aggregated and moved away from the immediate decision. The interface reads those updates asynchronously, so adding detail to Recent Activity does not require blocking the main thread or the network path.
The important constraint is simple: deciding whether a connection is allowed must never depend on making another internet request.
This also explains a phrase used throughout the product: NetBlocker blocks new connections. A connection that already exists does not become new because the protection toggle changed. For a clean test, the target app should be quit and reopened after the rule is enabled.
I could hide that distinction behind broader marketing language. Doing so would make the product sound stronger and make its behavior harder to understand.
“Block the internet” still needs a boundary
Network access is not one uniform thing. A useful rule has to distinguish outbound traffic from inbound traffic, external destinations from local loopback communication, and application traffic from operating system infrastructure.
Local communication is a good example. A main app may talk to one of its own helpers over the loopback interface. Breaking that path can damage local features even though no data is leaving the Mac. Name resolution and system-mediated networking introduce their own edge cases.
I have found it more useful to define and test the boundary explicitly than to start with “drop everything” and wait for applications to break. NetBlocker's promise is focused on preventing new outbound application connections from selected apps and their matched components, with narrowly defined local and infrastructure exceptions.
Temporary access has to respect the same grouping. “Allow for 5 Minutes”, “Allow for 1 Hour” and “Allow Until App Quits” would be misleading if they applied to the visible executable but left an associated helper in a different state.
A green toggle proves very little
One of the less glamorous parts of this project has been making every screen agree about whether protection is actually working.
A saved preference is not enforcement. The system extension may still require approval in System Settings. Its configuration may have failed. A trial may have ended. The menu bar helper may be showing stale state. In each case, a green “Protection on” label would be reassuring and false.
NetBlocker therefore tries to display effective state rather than requested state. If macOS approval is missing, the app says so and offers a route to the relevant settings. If protection is unavailable, the interface should not keep presenting blocked badges as if traffic were being dropped. When a temporary allowance changes, both the main window and status bar need to update immediately.
I underestimated how visible even a short delay would be. A menu bar icon that changes several seconds after the main toggle looks broken, even if the filter has already received the correct policy. More importantly, the user cannot tell which surface reflects reality.
The status bar component now remains available after the main window closes when there are registered apps, unless the user disables that behavior. It shows the protection state and provides quick temporary-access controls. This is a convenience feature, but it also gives the enforcement state a persistent, inspectable place in the system interface.
Recent Activity is confirmation, not a second firewall
Some outbound firewalls ask about each connection. That model is valuable when the user wants to inspect hosts, ports and individual processes. It also means that installing an app can generate a stream of questions before the user has enough context to answer them.
NetBlocker makes the decision at the app level. Once an app is in the blocked list, another approval dialog for each destination would undermine the point of the product.
Recent Activity exists to confirm what happened. It shows the app, the related process, the destination and the number of blocked attempts. Repeated attempts are combined, and retained history is bounded so a noisy process cannot turn the screen into an unusable log.
The empty state matters too. “No blocked connections yet” should mean that no matching new connection has been recorded. It must not be used as a generic placeholder when the extension is off or when activity reporting has failed. Security interfaces are full of short sentences whose accuracy depends on several systems agreeing behind them.
I do not want a remote list of blocked apps
The applications a person chooses to block can reveal more than it first appears. The list may expose their employer, creative tools, financial software, health products or games. Destinations in an activity log can be more sensitive still.
NetBlocker keeps its app list and Recent Activity on the Mac. The website can measure coarse events such as downloads and installations without receiving an inventory of blocked applications. The two data sets have no reason to meet.
Local storage is bounded, partly for performance and partly because retaining information indefinitely is rarely a neutral decision. The filter is not designed to send observed network activity off the Mac.
I prefer privacy properties that follow from what the product does not collect. A policy statement is a weaker protection if the underlying architecture already sends the data somewhere.
Is parental control a realistic next step?
The question comes up because some of the foundations are already visible: identify an app, express a policy, enforce it locally and report the result. It is tempting to imagine a parent changing that policy from another device.
I think the direction is technically credible. I do not think it is a small extension of NetBlocker.
Remote control introduces another security boundary. A production design would need secure device enrollment, authenticated family roles, signed policy updates, replay protection, key rotation, recovery for replaced devices and resistance to local tampering. It would also need a careful answer to what parents can see. End-to-end encryption (E2EE) should protect private policy and activity data in transit and at rest on the synchronization service, rather than asking families to trust the service operator with a readable copy.
Apple already provides useful privacy-preserving components for iPhone and iPad. Family Controls handles authorization, while Managed Settings and Device Activity can apply restrictions and monitor usage. The system can represent selected apps and websites using opaque tokens, so an app does not automatically receive a readable browsing and application inventory. Distribution also requires Apple's Family Controls entitlement.
Those constraints would shape the product. They are not implementation annoyances to bypass.
A version spanning Mac, iPhone, Android and Windows would need native enforcement on every platform. A shared policy and encrypted synchronization layer may be realistic, but the guarantees would differ by operating system. The interface would have to admit those differences instead of pretending that one remote toggle means exactly the same thing everywhere.
For now, I see parental control as a possible separate phase with its own threat model. It belongs in technical exploration, not in the current feature list.
What the simple interface is hiding
NetBlocker still presents the interaction I wanted at the beginning: drag in an app and block its new outbound connections. The interface does not ask the user to reason about process identifiers, signing requirements or helpers before making that choice.
The Photoshop bug taught me that hiding complexity is only useful after the software has dealt with it. Ignoring the complexity produces the same small interface with much less reliable behavior.
That has become my test for the product. Does “blocked” correspond to what the user selected? Does the displayed state correspond to what macOS is enforcing? Can the app explain a blocked attempt without collecting a remote history of the user's software? When the answer is uncertain, the interface should not pretend otherwise.
The difficult part was never drawing the drop target. It was making the drop target honest.