Vibecode Freedom
track this build4 phases, 12 steps, beginner friendly0%The visible distraction blocking loop is buildable, but a credible replacement needs more than the first screen. Freedom earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Freedom. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Freedom · indie build A blocking tool whose design problem is asymmetry: it must be harder to disable during a session than it was to start one, while never becoming unremovable, and it must block without acquiring the ability to see what you are doing. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Blocking | Name resolution and connection-level blocking, never traffic interception | reading encrypted traffic is a capability no blocker should hold | | Enforcement | A privileged component with a delayed disable path | friction is the mechanism and it has to survive a determined moment | | Sessions | Time-bounded with a stated end and no early exit shortcut | a session you can end instantly is a preference, not a session | | Sync | Optional, so a session started on one device holds on another | the second device is the obvious bypass | | Privacy | Block decisions logged locally in aggregate, never a browsing history | a blocker that records where you tried to go is surveillance | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the session logic. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A way to configure name resolution with elevated privileges** · free - Why: Enforcement requires changing system-level resolution, which is platform-specific and needs a component running with more privilege than the interface. - Get it: Your platform's mechanism for a privileged helper, and read what it grants before designing around it. - Verify: Resolution can be changed and restored by the helper - [ ] **Your documented removal procedure** · free - Why: A blocker with no escape becomes a fault the first time someone needs a blocked site urgently, and the procedure must not depend on the software working. - Get it: Write the procedure, test it with the application deliberately broken, and publish it where a user can find it while blocked. - Verify: Removal succeeds with the application non-functional - [ ] **A machine you can break and restore** · free if you have one - Why: Enforcement changes system configuration and testing the removal path means deliberately leaving it in a bad state. - Get it: A spare machine or a virtual machine you can snapshot and roll back. - Verify: You can install, break and fully restore without losing anything - [ ] **Your own blocklists** · free, and it requires being honest with yourself - Why: Blocking quality depends entirely on the lists, and a generic list blocks things you need while missing the things you actually reach for. - Get it: Write your own from honest observation of where you go when avoiding work, rather than downloading a category list. - Verify: A list exists derived from your own behaviour ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync - deep operating-system edge-case coverage If one of those is essential to you, that is the reason to keep paying for Freedom, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Freedom The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a blocker that holds when you want it least, without holding you hostage to replace Freedom. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deliberately makes itself hard to disable, which puts it in unusual territory: it is software whose purpose is to resist its own user. Two things follow. There must always be an escape. Not a convenient one, but a real one that does not require reinstalling an operating system. Someone will need an unexpectedly blocked site during an emergency, and a tool that cannot be removed under any circumstances is not a productivity aid, it is a fault. A delay, a restart or a typed confirmation is friction; no route at all is a trap. And it must never require the ability to intercept encrypted traffic. Installing a certificate that lets software read every connection the machine makes, for the sake of blocking a website, is a disproportionate and dangerous capability to hold. Block at the name resolution and connection layers, accept that this is coarser, and say so. Everything else in the build follows from those two: friction with an exit, and blocking without surveillance. ### Stack (fixed, do not substitute) - Blocking: Name resolution and connection-level blocking, never traffic interception · reading encrypted traffic is a capability no blocker should hold - Enforcement: A privileged component with a delayed disable path · friction is the mechanism and it has to survive a determined moment - Sessions: Time-bounded with a stated end and no early exit shortcut · a session you can end instantly is a preference, not a session - Sync: Optional, so a session started on one device holds on another · the second device is the obvious bypass - Privacy: Block decisions logged locally in aggregate, never a browsing history · a blocker that records where you tried to go is surveillance ### Data model (create this before Phase 1) - blocklists(id, name, entries_json, source, updated_at) - sessions(id, started_at, ends_at, blocklist_ids_json, strictness, ended_at, ended_reason) - schedules(id, name, days_json, start_local, end_local, blocklist_ids_json, enabled) - attempts(day, blocklist_id, count) · aggregate counts only, never individual destinations with times - exceptions(id, entry, reason, granted_at, expires_at, requires_delay) · a permanent escape, deliberately awkward - devices(id, name, last_seen_at) · for sessions that span machines - There is deliberately no table recording which sites were attempted and when, because that record would be a browsing history held by a tool whose job is not to watch you. ### Phase 1 · Blocking without reading anything Build: Block by hostname at the resolver and by connection where necessary. Install no certificate and inspect no traffic contents. Handle a site that resolves to shared addresses and one that uses a resolver you do not control. Trap: Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. Done when: Blocking works without installing any certificate or inspecting traffic contents, verified by inspecting what the installation adds. A site sharing addresses with others is blocked by name rather than by address where possible, and the limitation is stated. A device using a different resolver is handled or the limitation is reported. Nothing about the contents of any connection is recorded. Do not build yet: No sessions, friction, scheduling or sync. ### Phase 2 · Friction with a real exit Build: A delayed disable requiring a wait and a deliberate confirmation. No instant exit during a session. A documented removal procedure that works without the application, and a test that it does. Trap: A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. Done when: Ending a session early requires a stated delay and a deliberate confirmation. There is no instant exit, verified by looking for one. A documented removal procedure works without the application running, tested by breaking the application deliberately. The delay is configurable at session start but not during a session. Do not build yet: No scheduling, sync or reporting yet. ### Phase 3 · Sessions and schedules that survive a restart Build: Persist session state so a restart resumes rather than clears it. Schedules in local time handling daylight transitions. Handle a machine that was asleep across a scheduled start. Trap: A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. Done when: A session persists across a restart and blocking is applied before the network is usable. Schedules run in local time and handle both daylight transitions correctly. A machine asleep across a scheduled start applies the schedule on waking rather than skipping it. A session whose end time passed while the machine was off ends cleanly. Do not build yet: No sync or reporting yet. ### Phase 4 · The second device Build: Optional synchronisation of session state across devices you own. Handle a device that was offline when a session started, and one that goes offline mid-session. Never let a device that cannot reach sync silently unblock. Trap: A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. Done when: A session started on one device applies to another within a stated time. A device offline at session start applies the session when it reconnects. A device that goes offline mid-session keeps enforcing rather than unblocking. Sync is optional and the tool is fully usable on one device without it. Do not build yet: No reporting or exceptions yet. ### Phase 5 · Exceptions and reporting without surveillance Build: A permanent exception path with the same delay as ending a session. Aggregate reporting of attempt counts per list, never individual destinations with timestamps. Assert that no browsing history is stored anywhere. Trap: Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more. Done when: An exception can be added with the same delay as ending a session, and the exception is recorded. Reporting shows aggregate attempt counts per list per day with no individual destinations or timestamps. A test fails if any per-destination record with a timestamp is stored. Exports contain configuration and aggregates only. Do not build yet: Nothing after this. ### Out of scope (and why) - Traffic interception for content-level blocking, which requires a capability disproportionate to the purpose. - Blocking that cannot be removed, which is a fault rather than a feature. - Monitoring for someone else, such as an employer or a parent, which is a different product with different consent requirements. - Mobile enforcement, where platform restrictions make the guarantees much weaker and the honest answer is that it is easier to bypass. ### README must contain - That blocking happens at name resolution and connection level, with no certificate and no traffic inspection - That there is always a removal path and it works without the application running - That ending a session early requires a delay set before the session rather than during it - That a device losing its connection keeps enforcing rather than unblocking - That attempts are counted in aggregate and no browsing history is stored anywhere ===== AGENTS.md ===== # Agent instructions · Freedom indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Name resolution and connection-level blocking, never traffic interception, A privileged component with a delayed disable path, Time-bounded with a stated end and no early exit shortcut, Optional, so a session started on one device holds on another, Block decisions logged locally in aggregate, never a browsing history. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. ===== BUILD_PLAN.md ===== # Build plan · Freedom A blocking tool whose design problem is asymmetry: it must be harder to disable during a session than it was to start one, while never becoming unremovable, and it must block without acquiring the ability to see what you are doing. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Blocking without reading anything Sites are blocked at the resolution and connection layers, with no traffic inspection. ### Steps 1. Block at name resolution first Coarse, and it requires no ability to read anything. Files: `src/resolver.ts` 2. Install no certificate Reading every connection is disproportionate to blocking a website. Files: `tests/no-interception.test.ts` 3. State the limitations Shared addresses and alternative resolvers are real gaps, not secrets. Files: `docs/limitations.md` ### Done when - [ ] Blocking works with no certificate installed - [ ] No connection contents are inspected or recorded - [ ] Shared address limitations are stated - [ ] Alternative resolvers are handled or reported ### Watch out - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. ## Phase 2 · Friction with a real exit Ending a session early is difficult and always possible. ### Steps 1. Delay the disable, do not prevent it Friction is the mechanism. Impossibility is a fault. Files: `src/sessions.ts` 2. Set the delay before the session, not during Deciding it in the moment defeats the purpose. Files: `src/sessions.ts` 3. Test removal with the application broken The escape route must not depend on the software cooperating. Files: `tests/removal.md` ### Done when - [ ] Early exit requires a delay and confirmation - [ ] No instant exit exists - [ ] Removal works with the application broken - [ ] The delay is fixed once a session starts ### Watch out - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. ## Phase 3 · Sessions and schedules that survive a restart A session holds through a reboot and across a schedule boundary. ### Steps 1. Persist and re-apply before the network Restarting is the first bypass anyone finds. Files: `src/persist.ts` 2. Handle sleep across boundaries A laptop closed at nine and opened at eleven is the normal case. Files: `src/schedule.ts`, `src/schedule.test.ts` 3. End cleanly after an off period A session that expired while the machine was off should not linger. Files: `src/sessions.ts` ### Done when - [ ] A session survives a restart and applies before the network - [ ] Both daylight transitions are handled - [ ] Sleep across a scheduled start is handled - [ ] Expired sessions end cleanly after downtime ### Watch out - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. ## Phase 4 · The second device A session started on one machine holds on the others. ### Steps 1. Fail closed on connection loss Otherwise turning off the network is the bypass. Files: `src/sync.ts` 2. Apply on reconnection A device that was off should catch up rather than miss the session. Files: `src/sync.ts` 3. Keep sync optional One device should work completely without an account. Files: `src/sync.ts` ### Done when - [ ] A session applies across devices within a stated time - [ ] An offline device applies on reconnection - [ ] Losing connection keeps enforcement rather than unblocking - [ ] Single-device use works without sync ### Watch out - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. ## Not in this build - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. ## After v1, if you want it - Allowlist mode, which is far more effective and far more disruptive - Session presets for different kinds of work, since the lists genuinely differ - A weekly aggregate showing which list was hit most, which is useful without being a history ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sessions, schedules and aggregate counts. No browsing history. DB_PATH=~/.local/share/blocker/state.db # Required. Set before a session starts and fixed for its duration. EARLY_EXIT_DELAY_MINUTES=15 # Required. Present as a constant. Reading every connection is disproportionate to blocking a site. INSTALL_CERTIFICATE=never # Required. Present as a constant. A per-destination log is the most sensitive record on the machine. STORE_ATTEMPT_DESTINATIONS=never # Required. Otherwise turning off the network is the bypass. OFFLINE_BEHAVIOUR=keep_enforcing # Optional. Optional. One device works completely without it. SYNC_REMOTE=s3://my-blocker/state
You are building a lean indie version of Freedom. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Freedom · indie build A blocking tool whose design problem is asymmetry: it must be harder to disable during a session than it was to start one, while never becoming unremovable, and it must block without acquiring the ability to see what you are doing. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Blocking | Name resolution and connection-level blocking, never traffic interception | reading encrypted traffic is a capability no blocker should hold | | Enforcement | A privileged component with a delayed disable path | friction is the mechanism and it has to survive a determined moment | | Sessions | Time-bounded with a stated end and no early exit shortcut | a session you can end instantly is a preference, not a session | | Sync | Optional, so a session started on one device holds on another | the second device is the obvious bypass | | Privacy | Block decisions logged locally in aggregate, never a browsing history | a blocker that records where you tried to go is surveillance | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the session logic. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A way to configure name resolution with elevated privileges** · free - Why: Enforcement requires changing system-level resolution, which is platform-specific and needs a component running with more privilege than the interface. - Get it: Your platform's mechanism for a privileged helper, and read what it grants before designing around it. - Verify: Resolution can be changed and restored by the helper - [ ] **Your documented removal procedure** · free - Why: A blocker with no escape becomes a fault the first time someone needs a blocked site urgently, and the procedure must not depend on the software working. - Get it: Write the procedure, test it with the application deliberately broken, and publish it where a user can find it while blocked. - Verify: Removal succeeds with the application non-functional - [ ] **A machine you can break and restore** · free if you have one - Why: Enforcement changes system configuration and testing the removal path means deliberately leaving it in a bad state. - Get it: A spare machine or a virtual machine you can snapshot and roll back. - Verify: You can install, break and fully restore without losing anything - [ ] **Your own blocklists** · free, and it requires being honest with yourself - Why: Blocking quality depends entirely on the lists, and a generic list blocks things you need while missing the things you actually reach for. - Get it: Write your own from honest observation of where you go when avoiding work, rather than downloading a category list. - Verify: A list exists derived from your own behaviour ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync - deep operating-system edge-case coverage If one of those is essential to you, that is the reason to keep paying for Freedom, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Freedom The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a blocker that holds when you want it least, without holding you hostage to replace Freedom. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deliberately makes itself hard to disable, which puts it in unusual territory: it is software whose purpose is to resist its own user. Two things follow. There must always be an escape. Not a convenient one, but a real one that does not require reinstalling an operating system. Someone will need an unexpectedly blocked site during an emergency, and a tool that cannot be removed under any circumstances is not a productivity aid, it is a fault. A delay, a restart or a typed confirmation is friction; no route at all is a trap. And it must never require the ability to intercept encrypted traffic. Installing a certificate that lets software read every connection the machine makes, for the sake of blocking a website, is a disproportionate and dangerous capability to hold. Block at the name resolution and connection layers, accept that this is coarser, and say so. Everything else in the build follows from those two: friction with an exit, and blocking without surveillance. ### Stack (fixed, do not substitute) - Blocking: Name resolution and connection-level blocking, never traffic interception · reading encrypted traffic is a capability no blocker should hold - Enforcement: A privileged component with a delayed disable path · friction is the mechanism and it has to survive a determined moment - Sessions: Time-bounded with a stated end and no early exit shortcut · a session you can end instantly is a preference, not a session - Sync: Optional, so a session started on one device holds on another · the second device is the obvious bypass - Privacy: Block decisions logged locally in aggregate, never a browsing history · a blocker that records where you tried to go is surveillance ### Data model (create this before Phase 1) - blocklists(id, name, entries_json, source, updated_at) - sessions(id, started_at, ends_at, blocklist_ids_json, strictness, ended_at, ended_reason) - schedules(id, name, days_json, start_local, end_local, blocklist_ids_json, enabled) - attempts(day, blocklist_id, count) · aggregate counts only, never individual destinations with times - exceptions(id, entry, reason, granted_at, expires_at, requires_delay) · a permanent escape, deliberately awkward - devices(id, name, last_seen_at) · for sessions that span machines - There is deliberately no table recording which sites were attempted and when, because that record would be a browsing history held by a tool whose job is not to watch you. ### Phase 1 · Blocking without reading anything Build: Block by hostname at the resolver and by connection where necessary. Install no certificate and inspect no traffic contents. Handle a site that resolves to shared addresses and one that uses a resolver you do not control. Trap: Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. Done when: Blocking works without installing any certificate or inspecting traffic contents, verified by inspecting what the installation adds. A site sharing addresses with others is blocked by name rather than by address where possible, and the limitation is stated. A device using a different resolver is handled or the limitation is reported. Nothing about the contents of any connection is recorded. Do not build yet: No sessions, friction, scheduling or sync. ### Phase 2 · Friction with a real exit Build: A delayed disable requiring a wait and a deliberate confirmation. No instant exit during a session. A documented removal procedure that works without the application, and a test that it does. Trap: A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. Done when: Ending a session early requires a stated delay and a deliberate confirmation. There is no instant exit, verified by looking for one. A documented removal procedure works without the application running, tested by breaking the application deliberately. The delay is configurable at session start but not during a session. Do not build yet: No scheduling, sync or reporting yet. ### Phase 3 · Sessions and schedules that survive a restart Build: Persist session state so a restart resumes rather than clears it. Schedules in local time handling daylight transitions. Handle a machine that was asleep across a scheduled start. Trap: A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. Done when: A session persists across a restart and blocking is applied before the network is usable. Schedules run in local time and handle both daylight transitions correctly. A machine asleep across a scheduled start applies the schedule on waking rather than skipping it. A session whose end time passed while the machine was off ends cleanly. Do not build yet: No sync or reporting yet. ### Phase 4 · The second device Build: Optional synchronisation of session state across devices you own. Handle a device that was offline when a session started, and one that goes offline mid-session. Never let a device that cannot reach sync silently unblock. Trap: A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. Done when: A session started on one device applies to another within a stated time. A device offline at session start applies the session when it reconnects. A device that goes offline mid-session keeps enforcing rather than unblocking. Sync is optional and the tool is fully usable on one device without it. Do not build yet: No reporting or exceptions yet. ### Phase 5 · Exceptions and reporting without surveillance Build: A permanent exception path with the same delay as ending a session. Aggregate reporting of attempt counts per list, never individual destinations with timestamps. Assert that no browsing history is stored anywhere. Trap: Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more. Done when: An exception can be added with the same delay as ending a session, and the exception is recorded. Reporting shows aggregate attempt counts per list per day with no individual destinations or timestamps. A test fails if any per-destination record with a timestamp is stored. Exports contain configuration and aggregates only. Do not build yet: Nothing after this. ### Out of scope (and why) - Traffic interception for content-level blocking, which requires a capability disproportionate to the purpose. - Blocking that cannot be removed, which is a fault rather than a feature. - Monitoring for someone else, such as an employer or a parent, which is a different product with different consent requirements. - Mobile enforcement, where platform restrictions make the guarantees much weaker and the honest answer is that it is easier to bypass. ### README must contain - That blocking happens at name resolution and connection level, with no certificate and no traffic inspection - That there is always a removal path and it works without the application running - That ending a session early requires a delay set before the session rather than during it - That a device losing its connection keeps enforcing rather than unblocking - That attempts are counted in aggregate and no browsing history is stored anywhere ===== AGENTS.md ===== # Agent instructions · Freedom indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Name resolution and connection-level blocking, never traffic interception, A privileged component with a delayed disable path, Time-bounded with a stated end and no early exit shortcut, Optional, so a session started on one device holds on another, Block decisions logged locally in aggregate, never a browsing history. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. ===== BUILD_PLAN.md ===== # Build plan · Freedom A blocking tool whose design problem is asymmetry: it must be harder to disable during a session than it was to start one, while never becoming unremovable, and it must block without acquiring the ability to see what you are doing. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Blocking without reading anything Sites are blocked at the resolution and connection layers, with no traffic inspection. ### Steps 1. Block at name resolution first Coarse, and it requires no ability to read anything. Files: `src/resolver.ts` 2. Install no certificate Reading every connection is disproportionate to blocking a website. Files: `tests/no-interception.test.ts` 3. State the limitations Shared addresses and alternative resolvers are real gaps, not secrets. Files: `docs/limitations.md` ### Done when - [ ] Blocking works with no certificate installed - [ ] No connection contents are inspected or recorded - [ ] Shared address limitations are stated - [ ] Alternative resolvers are handled or reported ### Watch out - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. ## Phase 2 · Friction with a real exit Ending a session early is difficult and always possible. ### Steps 1. Delay the disable, do not prevent it Friction is the mechanism. Impossibility is a fault. Files: `src/sessions.ts` 2. Set the delay before the session, not during Deciding it in the moment defeats the purpose. Files: `src/sessions.ts` 3. Test removal with the application broken The escape route must not depend on the software cooperating. Files: `tests/removal.md` ### Done when - [ ] Early exit requires a delay and confirmation - [ ] No instant exit exists - [ ] Removal works with the application broken - [ ] The delay is fixed once a session starts ### Watch out - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. ## Phase 3 · Sessions and schedules that survive a restart A session holds through a reboot and across a schedule boundary. ### Steps 1. Persist and re-apply before the network Restarting is the first bypass anyone finds. Files: `src/persist.ts` 2. Handle sleep across boundaries A laptop closed at nine and opened at eleven is the normal case. Files: `src/schedule.ts`, `src/schedule.test.ts` 3. End cleanly after an off period A session that expired while the machine was off should not linger. Files: `src/sessions.ts` ### Done when - [ ] A session survives a restart and applies before the network - [ ] Both daylight transitions are handled - [ ] Sleep across a scheduled start is handled - [ ] Expired sessions end cleanly after downtime ### Watch out - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. ## Phase 4 · The second device A session started on one machine holds on the others. ### Steps 1. Fail closed on connection loss Otherwise turning off the network is the bypass. Files: `src/sync.ts` 2. Apply on reconnection A device that was off should catch up rather than miss the session. Files: `src/sync.ts` 3. Keep sync optional One device should work completely without an account. Files: `src/sync.ts` ### Done when - [ ] A session applies across devices within a stated time - [ ] An offline device applies on reconnection - [ ] Losing connection keeps enforcement rather than unblocking - [ ] Single-device use works without sync ### Watch out - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. ## Not in this build - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. ## After v1, if you want it - Allowlist mode, which is far more effective and far more disruptive - Session presets for different kinds of work, since the lists genuinely differ - A weekly aggregate showing which list was hit most, which is useful without being a history ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sessions, schedules and aggregate counts. No browsing history. DB_PATH=~/.local/share/blocker/state.db # Required. Set before a session starts and fixed for its duration. EARLY_EXIT_DELAY_MINUTES=15 # Required. Present as a constant. Reading every connection is disproportionate to blocking a site. INSTALL_CERTIFICATE=never # Required. Present as a constant. A per-destination log is the most sensitive record on the machine. STORE_ATTEMPT_DESTINATIONS=never # Required. Otherwise turning off the network is the bypass. OFFLINE_BEHAVIOUR=keep_enforcing # Optional. Optional. One device works completely without it. SYNC_REMOTE=s3://my-blocker/state
You are building a production product version of Freedom. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Freedom · product brief ## Problem The visible distraction blocking loop is buildable, but a credible replacement needs more than the first screen. Freedom earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A blocker that survives a restart, a router being switched off and a determined moment, that never reads a connection or records where you tried to go, and that can always be removed by someone who genuinely needs to. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux development machine - Node.js 22 and Rust toolchain - Local filesystem access and OS permissions ## Explicit non-goals for v1 - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync - deep operating-system edge-case coverage ## Success criteria - Blocking is confirmed to work with no certificate installed - Removal is confirmed to work with the application broken - A session is confirmed to survive a restart - Connection loss is confirmed to keep enforcement - The suite is confirmed to fail if a per-destination record is added ===== BRIEF.md ===== # Build brief · Freedom The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a blocker that holds when you want it least, without holding you hostage to replace Freedom. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deliberately makes itself hard to disable, which puts it in unusual territory: it is software whose purpose is to resist its own user. Two things follow. There must always be an escape. Not a convenient one, but a real one that does not require reinstalling an operating system. Someone will need an unexpectedly blocked site during an emergency, and a tool that cannot be removed under any circumstances is not a productivity aid, it is a fault. A delay, a restart or a typed confirmation is friction; no route at all is a trap. And it must never require the ability to intercept encrypted traffic. Installing a certificate that lets software read every connection the machine makes, for the sake of blocking a website, is a disproportionate and dangerous capability to hold. Block at the name resolution and connection layers, accept that this is coarser, and say so. Everything else in the build follows from those two: friction with an exit, and blocking without surveillance. ### Stack (fixed, do not substitute) - Blocking: Name resolution and connection-level blocking, never traffic interception · reading encrypted traffic is a capability no blocker should hold - Enforcement: A privileged component with a delayed disable path · friction is the mechanism and it has to survive a determined moment - Sessions: Time-bounded with a stated end and no early exit shortcut · a session you can end instantly is a preference, not a session - Sync: Optional, so a session started on one device holds on another · the second device is the obvious bypass - Privacy: Block decisions logged locally in aggregate, never a browsing history · a blocker that records where you tried to go is surveillance ### Data model (create this before Phase 1) - blocklists(id, name, entries_json, source, updated_at) - sessions(id, started_at, ends_at, blocklist_ids_json, strictness, ended_at, ended_reason) - schedules(id, name, days_json, start_local, end_local, blocklist_ids_json, enabled) - attempts(day, blocklist_id, count) · aggregate counts only, never individual destinations with times - exceptions(id, entry, reason, granted_at, expires_at, requires_delay) · a permanent escape, deliberately awkward - devices(id, name, last_seen_at) · for sessions that span machines - There is deliberately no table recording which sites were attempted and when, because that record would be a browsing history held by a tool whose job is not to watch you. ### Phase 1 · Blocking without reading anything Build: Block by hostname at the resolver and by connection where necessary. Install no certificate and inspect no traffic contents. Handle a site that resolves to shared addresses and one that uses a resolver you do not control. Trap: Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. Done when: Blocking works without installing any certificate or inspecting traffic contents, verified by inspecting what the installation adds. A site sharing addresses with others is blocked by name rather than by address where possible, and the limitation is stated. A device using a different resolver is handled or the limitation is reported. Nothing about the contents of any connection is recorded. Do not build yet: No sessions, friction, scheduling or sync. ### Phase 2 · Friction with a real exit Build: A delayed disable requiring a wait and a deliberate confirmation. No instant exit during a session. A documented removal procedure that works without the application, and a test that it does. Trap: A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. Done when: Ending a session early requires a stated delay and a deliberate confirmation. There is no instant exit, verified by looking for one. A documented removal procedure works without the application running, tested by breaking the application deliberately. The delay is configurable at session start but not during a session. Do not build yet: No scheduling, sync or reporting yet. ### Phase 3 · Sessions and schedules that survive a restart Build: Persist session state so a restart resumes rather than clears it. Schedules in local time handling daylight transitions. Handle a machine that was asleep across a scheduled start. Trap: A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. Done when: A session persists across a restart and blocking is applied before the network is usable. Schedules run in local time and handle both daylight transitions correctly. A machine asleep across a scheduled start applies the schedule on waking rather than skipping it. A session whose end time passed while the machine was off ends cleanly. Do not build yet: No sync or reporting yet. ### Phase 4 · The second device Build: Optional synchronisation of session state across devices you own. Handle a device that was offline when a session started, and one that goes offline mid-session. Never let a device that cannot reach sync silently unblock. Trap: A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. Done when: A session started on one device applies to another within a stated time. A device offline at session start applies the session when it reconnects. A device that goes offline mid-session keeps enforcing rather than unblocking. Sync is optional and the tool is fully usable on one device without it. Do not build yet: No reporting or exceptions yet. ### Phase 5 · Exceptions and reporting without surveillance Build: A permanent exception path with the same delay as ending a session. Aggregate reporting of attempt counts per list, never individual destinations with timestamps. Assert that no browsing history is stored anywhere. Trap: Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more. Done when: An exception can be added with the same delay as ending a session, and the exception is recorded. Reporting shows aggregate attempt counts per list per day with no individual destinations or timestamps. A test fails if any per-destination record with a timestamp is stored. Exports contain configuration and aggregates only. Do not build yet: Nothing after this. ### Out of scope (and why) - Traffic interception for content-level blocking, which requires a capability disproportionate to the purpose. - Blocking that cannot be removed, which is a fault rather than a feature. - Monitoring for someone else, such as an employer or a parent, which is a different product with different consent requirements. - Mobile enforcement, where platform restrictions make the guarantees much weaker and the honest answer is that it is easier to bypass. ### README must contain - That blocking happens at name resolution and connection level, with no certificate and no traffic inspection - That there is always a removal path and it works without the application running - That ending a session early requires a delay set before the session rather than during it - That a device losing its connection keeps enforcing rather than unblocking - That attempts are counted in aggregate and no browsing history is stored anywhere ===== ARCHITECTURE.md ===== # Architecture · Freedom ## Stack | Part | Choice | Why | | --- | --- | --- | | Blocking | Name resolution and connection-level blocking, never traffic interception | reading encrypted traffic is a capability no blocker should hold | | Enforcement | A privileged component with a delayed disable path | friction is the mechanism and it has to survive a determined moment | | Sessions | Time-bounded with a stated end and no early exit shortcut | a session you can end instantly is a preference, not a session | | Sync | Optional, so a session started on one device holds on another | the second device is the obvious bypass | | Privacy | Block decisions logged locally in aggregate, never a browsing history | a blocker that records where you tried to go is surveillance | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Blocking | Name and connection level enforcement. | Not swappable for traffic interception, whatever precision that would buy. | | Friction | Delayed exit with a real removal path. | Any delay, provided a documented removal exists and works without the application. | | Persistence | Sessions surviving restart and sleep. | Any implementation, provided blocking applies before the network is usable. | | Sync | Cross-device sessions that fail closed. | Any transport, provided connection loss keeps enforcement. | | Reporting | Aggregate counts without destinations. | Not swappable for per-destination logging, which is a browsing history. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Sessions, schedules and aggregate counts. No browsing history. - `EARLY_EXIT_DELAY_MINUTES` · required · Set before a session starts and fixed for its duration. - `INSTALL_CERTIFICATE` · required · Present as a constant. Reading every connection is disproportionate to blocking a site. - `STORE_ATTEMPT_DESTINATIONS` · required · Present as a constant. A per-destination log is the most sensitive record on the machine. - `OFFLINE_BEHAVIOUR` · required · Otherwise turning off the network is the bypass. - `SYNC_REMOTE` · optional · Optional. One device works completely without it. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Freedom product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Name resolution and connection-level blocking, never traffic interception, A privileged component with a delayed disable path, Time-bounded with a stated end and no early exit shortcut, Optional, so a session started on one device holds on another, Block decisions logged locally in aggregate, never a browsing history. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. - Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more. ===== MILESTONES.md ===== # Delivery milestones · Freedom Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Blocking without reading anything Sites are blocked at the resolution and connection layers, with no traffic inspection. ### Steps 1. Block at name resolution first Coarse, and it requires no ability to read anything. Files: `src/resolver.ts` 2. Install no certificate Reading every connection is disproportionate to blocking a website. Files: `tests/no-interception.test.ts` 3. State the limitations Shared addresses and alternative resolvers are real gaps, not secrets. Files: `docs/limitations.md` ### Done when - [ ] Blocking works with no certificate installed - [ ] No connection contents are inspected or recorded - [ ] Shared address limitations are stated - [ ] Alternative resolvers are handled or reported ### Watch out - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. ## M2 · Friction with a real exit Ending a session early is difficult and always possible. ### Steps 1. Delay the disable, do not prevent it Friction is the mechanism. Impossibility is a fault. Files: `src/sessions.ts` 2. Set the delay before the session, not during Deciding it in the moment defeats the purpose. Files: `src/sessions.ts` 3. Test removal with the application broken The escape route must not depend on the software cooperating. Files: `tests/removal.md` ### Done when - [ ] Early exit requires a delay and confirmation - [ ] No instant exit exists - [ ] Removal works with the application broken - [ ] The delay is fixed once a session starts ### Watch out - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. ## M3 · Sessions and schedules that survive a restart A session holds through a reboot and across a schedule boundary. ### Steps 1. Persist and re-apply before the network Restarting is the first bypass anyone finds. Files: `src/persist.ts` 2. Handle sleep across boundaries A laptop closed at nine and opened at eleven is the normal case. Files: `src/schedule.ts`, `src/schedule.test.ts` 3. End cleanly after an off period A session that expired while the machine was off should not linger. Files: `src/sessions.ts` ### Done when - [ ] A session survives a restart and applies before the network - [ ] Both daylight transitions are handled - [ ] Sleep across a scheduled start is handled - [ ] Expired sessions end cleanly after downtime ### Watch out - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. ## M4 · The second device A session started on one machine holds on the others. ### Steps 1. Fail closed on connection loss Otherwise turning off the network is the bypass. Files: `src/sync.ts` 2. Apply on reconnection A device that was off should catch up rather than miss the session. Files: `src/sync.ts` 3. Keep sync optional One device should work completely without an account. Files: `src/sync.ts` ### Done when - [ ] A session applies across devices within a stated time - [ ] An offline device applies on reconnection - [ ] Losing connection keeps enforcement rather than unblocking - [ ] Single-device use works without sync ### Watch out - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. ## M5 · Exceptions and reporting without surveillance (production only) You can unblock something you genuinely need, and the tool never records where you went. ### Steps 1. Give exceptions the same friction Otherwise the exception path is the instant exit you removed. Files: `src/exceptions.ts` 2. Count in aggregate only A per-destination log is a browsing history of the worst kind. Files: `src/attempts.ts` 3. Assert no browsing history exists In the suite, with a deliberately added field to prove the check works. Files: `tests/no-history.test.ts` ### Done when - [ ] Exceptions carry the same delay as ending a session - [ ] Reporting is aggregate per list per day - [ ] The suite fails if a per-destination record is added - [ ] Exports contain no browsing history ### Watch out - Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more. ===== OPERATIONS.md ===== # Operations · Freedom ## Backup Configuration and blocklists are the only data worth keeping, and they are small. Aggregate counts are mildly interesting. There is deliberately nothing sensitive to back up, which is a property worth preserving. ## Restore Restore and confirm no session state came back as active that should have expired, since a restored session that has already ended is enforcement nobody asked for and the exit delay applies to it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a personal tool the check is that enforcement is actually applied after a restart, which is the failure that makes the whole thing pointless and which nobody notices until they realise nothing is blocked. ## Incident checklist Being unable to reach something genuinely needed is the failure that matters most, and the documented removal path is the control. It should be tested on every release rather than assumed to still work. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Blocking is confirmed to work with no certificate installed - [ ] Removal is confirmed to work with the application broken - [ ] A session is confirmed to survive a restart - [ ] Connection loss is confirmed to keep enforcement - [ ] The suite is confirmed to fail if a per-destination record is added ## Launch constraint Do not market omitted Freedom capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sessions, schedules and aggregate counts. No browsing history. DB_PATH=~/.local/share/blocker/state.db # Required. Set before a session starts and fixed for its duration. EARLY_EXIT_DELAY_MINUTES=15 # Required. Present as a constant. Reading every connection is disproportionate to blocking a site. INSTALL_CERTIFICATE=never # Required. Present as a constant. A per-destination log is the most sensitive record on the machine. STORE_ATTEMPT_DESTINATIONS=never # Required. Otherwise turning off the network is the bypass. OFFLINE_BEHAVIOUR=keep_enforcing # Optional. Optional. One device works completely without it. SYNC_REMOTE=s3://my-blocker/state
# Freedom · indie build A blocking tool whose design problem is asymmetry: it must be harder to disable during a session than it was to start one, while never becoming unremovable, and it must block without acquiring the ability to see what you are doing. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Blocking | Name resolution and connection-level blocking, never traffic interception | reading encrypted traffic is a capability no blocker should hold | | Enforcement | A privileged component with a delayed disable path | friction is the mechanism and it has to survive a determined moment | | Sessions | Time-bounded with a stated end and no early exit shortcut | a session you can end instantly is a preference, not a session | | Sync | Optional, so a session started on one device holds on another | the second device is the obvious bypass | | Privacy | Block decisions logged locally in aggregate, never a browsing history | a blocker that records where you tried to go is surveillance | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the session logic. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A way to configure name resolution with elevated privileges** · free - Why: Enforcement requires changing system-level resolution, which is platform-specific and needs a component running with more privilege than the interface. - Get it: Your platform's mechanism for a privileged helper, and read what it grants before designing around it. - Verify: Resolution can be changed and restored by the helper - [ ] **Your documented removal procedure** · free - Why: A blocker with no escape becomes a fault the first time someone needs a blocked site urgently, and the procedure must not depend on the software working. - Get it: Write the procedure, test it with the application deliberately broken, and publish it where a user can find it while blocked. - Verify: Removal succeeds with the application non-functional - [ ] **A machine you can break and restore** · free if you have one - Why: Enforcement changes system configuration and testing the removal path means deliberately leaving it in a bad state. - Get it: A spare machine or a virtual machine you can snapshot and roll back. - Verify: You can install, break and fully restore without losing anything - [ ] **Your own blocklists** · free, and it requires being honest with yourself - Why: Blocking quality depends entirely on the lists, and a generic list blocks things you need while missing the things you actually reach for. - Get it: Write your own from honest observation of where you go when avoiding work, rather than downloading a category list. - Verify: A list exists derived from your own behaviour ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync - deep operating-system edge-case coverage If one of those is essential to you, that is the reason to keep paying for Freedom, and the README should say so rather than pretend.
# Build brief · Freedom The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a blocker that holds when you want it least, without holding you hostage to replace Freedom. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deliberately makes itself hard to disable, which puts it in unusual territory: it is software whose purpose is to resist its own user. Two things follow. There must always be an escape. Not a convenient one, but a real one that does not require reinstalling an operating system. Someone will need an unexpectedly blocked site during an emergency, and a tool that cannot be removed under any circumstances is not a productivity aid, it is a fault. A delay, a restart or a typed confirmation is friction; no route at all is a trap. And it must never require the ability to intercept encrypted traffic. Installing a certificate that lets software read every connection the machine makes, for the sake of blocking a website, is a disproportionate and dangerous capability to hold. Block at the name resolution and connection layers, accept that this is coarser, and say so. Everything else in the build follows from those two: friction with an exit, and blocking without surveillance. ### Stack (fixed, do not substitute) - Blocking: Name resolution and connection-level blocking, never traffic interception · reading encrypted traffic is a capability no blocker should hold - Enforcement: A privileged component with a delayed disable path · friction is the mechanism and it has to survive a determined moment - Sessions: Time-bounded with a stated end and no early exit shortcut · a session you can end instantly is a preference, not a session - Sync: Optional, so a session started on one device holds on another · the second device is the obvious bypass - Privacy: Block decisions logged locally in aggregate, never a browsing history · a blocker that records where you tried to go is surveillance ### Data model (create this before Phase 1) - blocklists(id, name, entries_json, source, updated_at) - sessions(id, started_at, ends_at, blocklist_ids_json, strictness, ended_at, ended_reason) - schedules(id, name, days_json, start_local, end_local, blocklist_ids_json, enabled) - attempts(day, blocklist_id, count) · aggregate counts only, never individual destinations with times - exceptions(id, entry, reason, granted_at, expires_at, requires_delay) · a permanent escape, deliberately awkward - devices(id, name, last_seen_at) · for sessions that span machines - There is deliberately no table recording which sites were attempted and when, because that record would be a browsing history held by a tool whose job is not to watch you. ### Phase 1 · Blocking without reading anything Build: Block by hostname at the resolver and by connection where necessary. Install no certificate and inspect no traffic contents. Handle a site that resolves to shared addresses and one that uses a resolver you do not control. Trap: Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. Done when: Blocking works without installing any certificate or inspecting traffic contents, verified by inspecting what the installation adds. A site sharing addresses with others is blocked by name rather than by address where possible, and the limitation is stated. A device using a different resolver is handled or the limitation is reported. Nothing about the contents of any connection is recorded. Do not build yet: No sessions, friction, scheduling or sync. ### Phase 2 · Friction with a real exit Build: A delayed disable requiring a wait and a deliberate confirmation. No instant exit during a session. A documented removal procedure that works without the application, and a test that it does. Trap: A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. Done when: Ending a session early requires a stated delay and a deliberate confirmation. There is no instant exit, verified by looking for one. A documented removal procedure works without the application running, tested by breaking the application deliberately. The delay is configurable at session start but not during a session. Do not build yet: No scheduling, sync or reporting yet. ### Phase 3 · Sessions and schedules that survive a restart Build: Persist session state so a restart resumes rather than clears it. Schedules in local time handling daylight transitions. Handle a machine that was asleep across a scheduled start. Trap: A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. Done when: A session persists across a restart and blocking is applied before the network is usable. Schedules run in local time and handle both daylight transitions correctly. A machine asleep across a scheduled start applies the schedule on waking rather than skipping it. A session whose end time passed while the machine was off ends cleanly. Do not build yet: No sync or reporting yet. ### Phase 4 · The second device Build: Optional synchronisation of session state across devices you own. Handle a device that was offline when a session started, and one that goes offline mid-session. Never let a device that cannot reach sync silently unblock. Trap: A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. Done when: A session started on one device applies to another within a stated time. A device offline at session start applies the session when it reconnects. A device that goes offline mid-session keeps enforcing rather than unblocking. Sync is optional and the tool is fully usable on one device without it. Do not build yet: No reporting or exceptions yet. ### Phase 5 · Exceptions and reporting without surveillance Build: A permanent exception path with the same delay as ending a session. Aggregate reporting of attempt counts per list, never individual destinations with timestamps. Assert that no browsing history is stored anywhere. Trap: Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more. Done when: An exception can be added with the same delay as ending a session, and the exception is recorded. Reporting shows aggregate attempt counts per list per day with no individual destinations or timestamps. A test fails if any per-destination record with a timestamp is stored. Exports contain configuration and aggregates only. Do not build yet: Nothing after this. ### Out of scope (and why) - Traffic interception for content-level blocking, which requires a capability disproportionate to the purpose. - Blocking that cannot be removed, which is a fault rather than a feature. - Monitoring for someone else, such as an employer or a parent, which is a different product with different consent requirements. - Mobile enforcement, where platform restrictions make the guarantees much weaker and the honest answer is that it is easier to bypass. ### README must contain - That blocking happens at name resolution and connection level, with no certificate and no traffic inspection - That there is always a removal path and it works without the application running - That ending a session early requires a delay set before the session rather than during it - That a device losing its connection keeps enforcing rather than unblocking - That attempts are counted in aggregate and no browsing history is stored anywhere
# Agent instructions · Freedom indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Name resolution and connection-level blocking, never traffic interception, A privileged component with a delayed disable path, Time-bounded with a stated end and no early exit shortcut, Optional, so a session started on one device holds on another, Block decisions logged locally in aggregate, never a browsing history. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew.
# Build plan · Freedom A blocking tool whose design problem is asymmetry: it must be harder to disable during a session than it was to start one, while never becoming unremovable, and it must block without acquiring the ability to see what you are doing. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Blocking without reading anything Sites are blocked at the resolution and connection layers, with no traffic inspection. ### Steps 1. Block at name resolution first Coarse, and it requires no ability to read anything. Files: `src/resolver.ts` 2. Install no certificate Reading every connection is disproportionate to blocking a website. Files: `tests/no-interception.test.ts` 3. State the limitations Shared addresses and alternative resolvers are real gaps, not secrets. Files: `docs/limitations.md` ### Done when - [ ] Blocking works with no certificate installed - [ ] No connection contents are inspected or recorded - [ ] Shared address limitations are stated - [ ] Alternative resolvers are handled or reported ### Watch out - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. ## Phase 2 · Friction with a real exit Ending a session early is difficult and always possible. ### Steps 1. Delay the disable, do not prevent it Friction is the mechanism. Impossibility is a fault. Files: `src/sessions.ts` 2. Set the delay before the session, not during Deciding it in the moment defeats the purpose. Files: `src/sessions.ts` 3. Test removal with the application broken The escape route must not depend on the software cooperating. Files: `tests/removal.md` ### Done when - [ ] Early exit requires a delay and confirmation - [ ] No instant exit exists - [ ] Removal works with the application broken - [ ] The delay is fixed once a session starts ### Watch out - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. ## Phase 3 · Sessions and schedules that survive a restart A session holds through a reboot and across a schedule boundary. ### Steps 1. Persist and re-apply before the network Restarting is the first bypass anyone finds. Files: `src/persist.ts` 2. Handle sleep across boundaries A laptop closed at nine and opened at eleven is the normal case. Files: `src/schedule.ts`, `src/schedule.test.ts` 3. End cleanly after an off period A session that expired while the machine was off should not linger. Files: `src/sessions.ts` ### Done when - [ ] A session survives a restart and applies before the network - [ ] Both daylight transitions are handled - [ ] Sleep across a scheduled start is handled - [ ] Expired sessions end cleanly after downtime ### Watch out - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. ## Phase 4 · The second device A session started on one machine holds on the others. ### Steps 1. Fail closed on connection loss Otherwise turning off the network is the bypass. Files: `src/sync.ts` 2. Apply on reconnection A device that was off should catch up rather than miss the session. Files: `src/sync.ts` 3. Keep sync optional One device should work completely without an account. Files: `src/sync.ts` ### Done when - [ ] A session applies across devices within a stated time - [ ] An offline device applies on reconnection - [ ] Losing connection keeps enforcement rather than unblocking - [ ] Single-device use works without sync ### Watch out - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. ## Not in this build - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. ## After v1, if you want it - Allowlist mode, which is far more effective and far more disruptive - Session presets for different kinds of work, since the lists genuinely differ - A weekly aggregate showing which list was hit most, which is useful without being a history
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sessions, schedules and aggregate counts. No browsing history. DB_PATH=~/.local/share/blocker/state.db # Required. Set before a session starts and fixed for its duration. EARLY_EXIT_DELAY_MINUTES=15 # Required. Present as a constant. Reading every connection is disproportionate to blocking a site. INSTALL_CERTIFICATE=never # Required. Present as a constant. A per-destination log is the most sensitive record on the machine. STORE_ATTEMPT_DESTINATIONS=never # Required. Otherwise turning off the network is the bypass. OFFLINE_BEHAVIOUR=keep_enforcing # Optional. Optional. One device works completely without it. SYNC_REMOTE=s3://my-blocker/state
# Freedom · product brief ## Problem The visible distraction blocking loop is buildable, but a credible replacement needs more than the first screen. Freedom earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A blocker that survives a restart, a router being switched off and a determined moment, that never reads a connection or records where you tried to go, and that can always be removed by someone who genuinely needs to. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux development machine - Node.js 22 and Rust toolchain - Local filesystem access and OS permissions ## Explicit non-goals for v1 - Traffic interception for content-level blocking. - Blocking that cannot be removed. - Monitoring on behalf of someone else. - Mobile enforcement with equivalent guarantees. - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync - deep operating-system edge-case coverage ## Success criteria - Blocking is confirmed to work with no certificate installed - Removal is confirmed to work with the application broken - A session is confirmed to survive a restart - Connection loss is confirmed to keep enforcement - The suite is confirmed to fail if a per-destination record is added
# Build brief · Freedom The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a blocker that holds when you want it least, without holding you hostage to replace Freedom. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deliberately makes itself hard to disable, which puts it in unusual territory: it is software whose purpose is to resist its own user. Two things follow. There must always be an escape. Not a convenient one, but a real one that does not require reinstalling an operating system. Someone will need an unexpectedly blocked site during an emergency, and a tool that cannot be removed under any circumstances is not a productivity aid, it is a fault. A delay, a restart or a typed confirmation is friction; no route at all is a trap. And it must never require the ability to intercept encrypted traffic. Installing a certificate that lets software read every connection the machine makes, for the sake of blocking a website, is a disproportionate and dangerous capability to hold. Block at the name resolution and connection layers, accept that this is coarser, and say so. Everything else in the build follows from those two: friction with an exit, and blocking without surveillance. ### Stack (fixed, do not substitute) - Blocking: Name resolution and connection-level blocking, never traffic interception · reading encrypted traffic is a capability no blocker should hold - Enforcement: A privileged component with a delayed disable path · friction is the mechanism and it has to survive a determined moment - Sessions: Time-bounded with a stated end and no early exit shortcut · a session you can end instantly is a preference, not a session - Sync: Optional, so a session started on one device holds on another · the second device is the obvious bypass - Privacy: Block decisions logged locally in aggregate, never a browsing history · a blocker that records where you tried to go is surveillance ### Data model (create this before Phase 1) - blocklists(id, name, entries_json, source, updated_at) - sessions(id, started_at, ends_at, blocklist_ids_json, strictness, ended_at, ended_reason) - schedules(id, name, days_json, start_local, end_local, blocklist_ids_json, enabled) - attempts(day, blocklist_id, count) · aggregate counts only, never individual destinations with times - exceptions(id, entry, reason, granted_at, expires_at, requires_delay) · a permanent escape, deliberately awkward - devices(id, name, last_seen_at) · for sessions that span machines - There is deliberately no table recording which sites were attempted and when, because that record would be a browsing history held by a tool whose job is not to watch you. ### Phase 1 · Blocking without reading anything Build: Block by hostname at the resolver and by connection where necessary. Install no certificate and inspect no traffic contents. Handle a site that resolves to shared addresses and one that uses a resolver you do not control. Trap: Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. Done when: Blocking works without installing any certificate or inspecting traffic contents, verified by inspecting what the installation adds. A site sharing addresses with others is blocked by name rather than by address where possible, and the limitation is stated. A device using a different resolver is handled or the limitation is reported. Nothing about the contents of any connection is recorded. Do not build yet: No sessions, friction, scheduling or sync. ### Phase 2 · Friction with a real exit Build: A delayed disable requiring a wait and a deliberate confirmation. No instant exit during a session. A documented removal procedure that works without the application, and a test that it does. Trap: A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. Done when: Ending a session early requires a stated delay and a deliberate confirmation. There is no instant exit, verified by looking for one. A documented removal procedure works without the application running, tested by breaking the application deliberately. The delay is configurable at session start but not during a session. Do not build yet: No scheduling, sync or reporting yet. ### Phase 3 · Sessions and schedules that survive a restart Build: Persist session state so a restart resumes rather than clears it. Schedules in local time handling daylight transitions. Handle a machine that was asleep across a scheduled start. Trap: A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. Done when: A session persists across a restart and blocking is applied before the network is usable. Schedules run in local time and handle both daylight transitions correctly. A machine asleep across a scheduled start applies the schedule on waking rather than skipping it. A session whose end time passed while the machine was off ends cleanly. Do not build yet: No sync or reporting yet. ### Phase 4 · The second device Build: Optional synchronisation of session state across devices you own. Handle a device that was offline when a session started, and one that goes offline mid-session. Never let a device that cannot reach sync silently unblock. Trap: A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. Done when: A session started on one device applies to another within a stated time. A device offline at session start applies the session when it reconnects. A device that goes offline mid-session keeps enforcing rather than unblocking. Sync is optional and the tool is fully usable on one device without it. Do not build yet: No reporting or exceptions yet. ### Phase 5 · Exceptions and reporting without surveillance Build: A permanent exception path with the same delay as ending a session. Aggregate reporting of attempt counts per list, never individual destinations with timestamps. Assert that no browsing history is stored anywhere. Trap: Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more. Done when: An exception can be added with the same delay as ending a session, and the exception is recorded. Reporting shows aggregate attempt counts per list per day with no individual destinations or timestamps. A test fails if any per-destination record with a timestamp is stored. Exports contain configuration and aggregates only. Do not build yet: Nothing after this. ### Out of scope (and why) - Traffic interception for content-level blocking, which requires a capability disproportionate to the purpose. - Blocking that cannot be removed, which is a fault rather than a feature. - Monitoring for someone else, such as an employer or a parent, which is a different product with different consent requirements. - Mobile enforcement, where platform restrictions make the guarantees much weaker and the honest answer is that it is easier to bypass. ### README must contain - That blocking happens at name resolution and connection level, with no certificate and no traffic inspection - That there is always a removal path and it works without the application running - That ending a session early requires a delay set before the session rather than during it - That a device losing its connection keeps enforcing rather than unblocking - That attempts are counted in aggregate and no browsing history is stored anywhere
# Architecture · Freedom ## Stack | Part | Choice | Why | | --- | --- | --- | | Blocking | Name resolution and connection-level blocking, never traffic interception | reading encrypted traffic is a capability no blocker should hold | | Enforcement | A privileged component with a delayed disable path | friction is the mechanism and it has to survive a determined moment | | Sessions | Time-bounded with a stated end and no early exit shortcut | a session you can end instantly is a preference, not a session | | Sync | Optional, so a session started on one device holds on another | the second device is the obvious bypass | | Privacy | Block decisions logged locally in aggregate, never a browsing history | a blocker that records where you tried to go is surveillance | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Blocking | Name and connection level enforcement. | Not swappable for traffic interception, whatever precision that would buy. | | Friction | Delayed exit with a real removal path. | Any delay, provided a documented removal exists and works without the application. | | Persistence | Sessions surviving restart and sleep. | Any implementation, provided blocking applies before the network is usable. | | Sync | Cross-device sessions that fail closed. | Any transport, provided connection loss keeps enforcement. | | Reporting | Aggregate counts without destinations. | Not swappable for per-destination logging, which is a browsing history. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Sessions, schedules and aggregate counts. No browsing history. - `EARLY_EXIT_DELAY_MINUTES` · required · Set before a session starts and fixed for its duration. - `INSTALL_CERTIFICATE` · required · Present as a constant. Reading every connection is disproportionate to blocking a site. - `STORE_ATTEMPT_DESTINATIONS` · required · Present as a constant. A per-destination log is the most sensitive record on the machine. - `OFFLINE_BEHAVIOUR` · required · Otherwise turning off the network is the bypass. - `SYNC_REMOTE` · optional · Optional. One device works completely without it. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Freedom product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Name resolution and connection-level blocking, never traffic interception, A privileged component with a delayed disable path, Time-bounded with a stated end and no early exit shortcut, Optional, so a session started on one device holds on another, Block decisions logged locally in aggregate, never a browsing history. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. - Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more.
# Delivery milestones · Freedom Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Blocking without reading anything Sites are blocked at the resolution and connection layers, with no traffic inspection. ### Steps 1. Block at name resolution first Coarse, and it requires no ability to read anything. Files: `src/resolver.ts` 2. Install no certificate Reading every connection is disproportionate to blocking a website. Files: `tests/no-interception.test.ts` 3. State the limitations Shared addresses and alternative resolvers are real gaps, not secrets. Files: `docs/limitations.md` ### Done when - [ ] Blocking works with no certificate installed - [ ] No connection contents are inspected or recorded - [ ] Shared address limitations are stated - [ ] Alternative resolvers are handled or reported ### Watch out - Intercepting traffic to block by page content requires a certificate that lets the software read every connection the machine makes, which is a capability far exceeding the purpose and a serious risk if the software is ever compromised. Block coarsely at the name and connection layers and accept the reduced precision. ## M2 · Friction with a real exit Ending a session early is difficult and always possible. ### Steps 1. Delay the disable, do not prevent it Friction is the mechanism. Impossibility is a fault. Files: `src/sessions.ts` 2. Set the delay before the session, not during Deciding it in the moment defeats the purpose. Files: `src/sessions.ts` 3. Test removal with the application broken The escape route must not depend on the software cooperating. Files: `tests/removal.md` ### Done when - [ ] Early exit requires a delay and confirmation - [ ] No instant exit exists - [ ] Removal works with the application broken - [ ] The delay is fixed once a session starts ### Watch out - A blocker with no removal path becomes malware the first time someone needs a blocked site in an emergency and cannot get to it, and the removal procedure must work even if the application itself is broken. Document it, test it, and make sure it does not require the software's cooperation. ## M3 · Sessions and schedules that survive a restart A session holds through a reboot and across a schedule boundary. ### Steps 1. Persist and re-apply before the network Restarting is the first bypass anyone finds. Files: `src/persist.ts` 2. Handle sleep across boundaries A laptop closed at nine and opened at eleven is the normal case. Files: `src/schedule.ts`, `src/schedule.test.ts` 3. End cleanly after an off period A session that expired while the machine was off should not linger. Files: `src/sessions.ts` ### Done when - [ ] A session survives a restart and applies before the network - [ ] Both daylight transitions are handled - [ ] Sleep across a scheduled start is handled - [ ] Expired sessions end cleanly after downtime ### Watch out - A session that clears on restart makes rebooting the bypass, and it is the first one anyone finds. Persist the session and re-apply blocking before the network is usable, so a restart costs nothing and gains nothing. ## M4 · The second device A session started on one machine holds on the others. ### Steps 1. Fail closed on connection loss Otherwise turning off the network is the bypass. Files: `src/sync.ts` 2. Apply on reconnection A device that was off should catch up rather than miss the session. Files: `src/sync.ts` 3. Keep sync optional One device should work completely without an account. Files: `src/sync.ts` ### Done when - [ ] A session applies across devices within a stated time - [ ] An offline device applies on reconnection - [ ] Losing connection keeps enforcement rather than unblocking - [ ] Single-device use works without sync ### Watch out - A device that loses its connection and therefore stops enforcing turns the network into the bypass, and it is a bypass you can trigger by turning off a router. Fail closed: a device that cannot confirm a session has ended keeps enforcing the last state it knew. ## M5 · Exceptions and reporting without surveillance (production only) You can unblock something you genuinely need, and the tool never records where you went. ### Steps 1. Give exceptions the same friction Otherwise the exception path is the instant exit you removed. Files: `src/exceptions.ts` 2. Count in aggregate only A per-destination log is a browsing history of the worst kind. Files: `src/attempts.ts` 3. Assert no browsing history exists In the suite, with a deliberately added field to prove the check works. Files: `tests/no-history.test.ts` ### Done when - [ ] Exceptions carry the same delay as ending a session - [ ] Reporting is aggregate per list per day - [ ] The suite fails if a per-destination record is added - [ ] Exports contain no browsing history ### Watch out - Logging every blocked attempt with its destination and time produces a browsing history of exactly the sites someone is trying to avoid, held by a tool they installed for self-discipline, and it is the most sensitive record on the machine. Count in aggregate per list per day and store nothing more.
# Operations · Freedom ## Backup Configuration and blocklists are the only data worth keeping, and they are small. Aggregate counts are mildly interesting. There is deliberately nothing sensitive to back up, which is a property worth preserving. ## Restore Restore and confirm no session state came back as active that should have expired, since a restored session that has already ended is enforcement nobody asked for and the exit delay applies to it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a personal tool the check is that enforcement is actually applied after a restart, which is the failure that makes the whole thing pointless and which nobody notices until they realise nothing is blocked. ## Incident checklist Being unable to reach something genuinely needed is the failure that matters most, and the documented removal path is the control. It should be tested on every release rather than assumed to still work. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Blocking is confirmed to work with no certificate installed - [ ] Removal is confirmed to work with the application broken - [ ] A session is confirmed to survive a restart - [ ] Connection loss is confirmed to keep enforcement - [ ] The suite is confirmed to fail if a per-destination record is added ## Launch constraint Do not market omitted Freedom capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sessions, schedules and aggregate counts. No browsing history. DB_PATH=~/.local/share/blocker/state.db # Required. Set before a session starts and fixed for its duration. EARLY_EXIT_DELAY_MINUTES=15 # Required. Present as a constant. Reading every connection is disproportionate to blocking a site. INSTALL_CERTIFICATE=never # Required. Present as a constant. A per-destination log is the most sensitive record on the machine. STORE_ATTEMPT_DESTINATIONS=never # Required. Otherwise turning off the network is the bypass. OFFLINE_BEHAVIOUR=keep_enforcing # Optional. Optional. One device works completely without it. SYNC_REMOTE=s3://my-blocker/state
$ choose a build depth, inspect the files, then open the complete pack in your agent
Freedom: People keep paying because a utility used dozens of times a day must be instant, predictable, and compatible with every OS update.
xpolished shortcuts and accessibility
xvendor-maintained updates
xcross-device sync
xdeep operating-system edge-case coverage
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Freedom pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | Unlimited devices, cross-device sync, and custom blocklists; the public page does not state a numeric session-count or duration cap. |
| premium | $8.99 | $3.33 | Unlimited blocking sessions, including sessions longer than 2 hours, across all supported devices. |
| forever | custom | — | Lifetime Premium access across supported devices. |
free tierUnlimited devices, cross-device sync, and custom blocklists; numeric session limits are not published.
billingmonthly + annual + lifetime; 7-day Premium trial
hidden costsThe $3.33 figure on file is the annual-billing equivalent, not the month-to-month price of $8.99. The $99.50 lifetime price is a current 50%-off promotion rather than the displayed $199 regular price.
verified 2026-08-14 · source ↗
Vibecode Freedom
Kinda. The core of Freedom is buildable in a weekend with the prompt on this page, but there are real gaps: polished shortcuts and accessibility, vendor-maintained updates. Read the honest list above before committing.
How much does Freedom cost?
Freedom costs about $3.33/month (Premium, checked 2026-07-31), which is $39.96 per year.
What do I lose by replacing Freedom?
Honestly: polished shortcuts and accessibility; vendor-maintained updates; cross-device sync; deep operating-system edge-case coverage. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Freedom?
Yes: Apple Screen Time (Already installed on Apple devices, syncs limits, and is easier to override than Freedom.) ScreenZen (App and site blocking across phones and desktops, paid for by donations instead of guilt.) The prompt is for when you want it exactly your way.