<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adem İşler</title>
    <description>The latest articles on DEV Community by Adem İşler (@ademisler).</description>
    <link>https://dev.to/ademisler</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3906784%2F4cdcc8bc-99f5-45bb-8696-6748452b1581.png</url>
      <title>DEV Community: Adem İşler</title>
      <link>https://dev.to/ademisler</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ademisler"/>
    <language>en</language>
    <item>
      <title>How RunOnMine Puts a Local Policy Boundary Between AI Agents and Your Machine</title>
      <dc:creator>Adem İşler</dc:creator>
      <pubDate>Sun, 09 Aug 2026 17:29:57 +0000</pubDate>
      <link>https://dev.to/ademisler/how-runonmine-puts-a-local-policy-boundary-between-ai-agents-and-your-machine-50mm</link>
      <guid>https://dev.to/ademisler/how-runonmine-puts-a-local-policy-boundary-between-ai-agents-and-your-machine-50mm</guid>
      <description>&lt;p&gt;AI agents have a boundary problem that becomes more visible as soon as they can touch a real machine.&lt;/p&gt;

&lt;p&gt;Keep the model inside a chat box and the boundary is simple, but the workflow is limited.&lt;/p&gt;

&lt;p&gt;Give the model access to files, a terminal, a browser and desktop automation and the workflow becomes much more useful — but a raw shell or broad credential can turn the entire operating-system account into one permission.&lt;/p&gt;

&lt;p&gt;That creates a practical trade-off:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;capability or control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to see whether machine access could stay useful without making account-level authority the default interface.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;RunOnMine&lt;/strong&gt;, an open-source, local-first MCP security gateway for controlled AI access to files, terminals, browsers and desktop apps.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/ademisler/RunOnMine" rel="noopener noreferrer"&gt;https://github.com/ademisler/RunOnMine&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first public beta is available for macOS, Windows and Linux.&lt;/p&gt;

&lt;p&gt;This article is about the architecture behind the boundary, the failure modes I wanted to avoid, and why I do not describe RunOnMine as a sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product boundary
&lt;/h2&gt;

&lt;p&gt;RunOnMine is not a collection of MCP tools with a permission dialog added later.&lt;/p&gt;

&lt;p&gt;The permission boundary is the product.&lt;/p&gt;

&lt;p&gt;A request should not become executable just because an authenticated client knows the name of a tool.&lt;/p&gt;

&lt;p&gt;The system needs to answer several questions together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which connector delivered the request?&lt;/li&gt;
&lt;li&gt;Which requester is behind that connector?&lt;/li&gt;
&lt;li&gt;Which tool is being called?&lt;/li&gt;
&lt;li&gt;Which resource is being targeted?&lt;/li&gt;
&lt;li&gt;What does the machine owner's local policy say?&lt;/li&gt;
&lt;li&gt;Does this exact action require approval?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simplified flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI request
    |
    v
requester + connector identity
    |
    v
local resource policy
    |
    +---- allow ----&amp;gt; execute
    |
    +---- ask ------&amp;gt; local approval ----&amp;gt; execute / deny
    |
    +---- deny --------------------------&amp;gt; stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is where the decision happens.&lt;/p&gt;

&lt;p&gt;It happens on the machine being controlled.&lt;/p&gt;

&lt;p&gt;Remote authentication can narrow authority, but it does not get to expand local policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tool name is not an authorization decision
&lt;/h2&gt;

&lt;p&gt;Consider a filesystem write.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fs_write&lt;/code&gt; tells us what class of operation is requested. It does not tell us whether the request should be allowed.&lt;/p&gt;

&lt;p&gt;The authorization decision also depends on the path, the requester, the connector and the exact action.&lt;/p&gt;

&lt;p&gt;The same is true for shell execution.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git status&lt;/code&gt; inside one selected repository and a destructive command from a remote connector should not collapse into the same boolean permission just because both use &lt;code&gt;shell_exec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is why RunOnMine separates tool capability from local authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selected roots instead of account-wide filesystem access
&lt;/h2&gt;

&lt;p&gt;A coding agent usually does not need my entire home directory.&lt;/p&gt;

&lt;p&gt;It may need one repository.&lt;/p&gt;

&lt;p&gt;RunOnMine asks the owner to select filesystem roots and keeps file operations inside those roots. The implementation uses capability-oriented, descriptor-relative operations and rejects paths that escape the configured boundary.&lt;/p&gt;

&lt;p&gt;This changes the failure surface.&lt;/p&gt;

&lt;p&gt;A prompt mistake or prompt injection inside one project should not automatically turn into visibility over unrelated repositories, SSH material, browser state and personal documents simply because the process happens to run as the same OS user.&lt;/p&gt;

&lt;p&gt;The rule I wanted was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;grant the project, not the account.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval should be about the action, not the button
&lt;/h2&gt;

&lt;p&gt;Human-in-the-loop security becomes weak if approval means "trust this tool forever".&lt;/p&gt;

&lt;p&gt;An owner needs to understand what is about to happen.&lt;/p&gt;

&lt;p&gt;RunOnMine binds approval to the requester and the concrete action context. If the meaningful action changes, the previous approval should not silently become permission for the new one.&lt;/p&gt;

&lt;p&gt;Approval is also local-only.&lt;/p&gt;

&lt;p&gt;There is no MCP tool called &lt;code&gt;approve_my_request&lt;/code&gt; that a remote agent can invoke after asking for something dangerous.&lt;/p&gt;

&lt;p&gt;That sounds obvious when written down, but keeping the approval authority outside the remote tool surface is an important part of the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shell execution is not a sandbox
&lt;/h2&gt;

&lt;p&gt;This is the limitation I care most about stating clearly.&lt;/p&gt;

&lt;p&gt;If a shell command is permitted, it runs with the authority of the RunOnMine operating-system account.&lt;/p&gt;

&lt;p&gt;RunOnMine can decide whether a request reaches execution. It can scope the working directory, bind the decision to the requester, require approval, bound retained output and terminate a process tree on timeout.&lt;/p&gt;

&lt;p&gt;It cannot turn an arbitrary permitted command into harmless code.&lt;/p&gt;

&lt;p&gt;That is why the project calls itself a security gateway and approval system rather than a sandbox.&lt;/p&gt;

&lt;p&gt;The optional privileged helper is a separate installation boundary. It is not installed by normal setup, and remote connectors do not receive administrator execution through the standard policy profiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser control adds a network boundary
&lt;/h2&gt;

&lt;p&gt;A separate browser profile is useful, but it is not the whole browser security problem.&lt;/p&gt;

&lt;p&gt;If a remotely driven browser can freely reach localhost or private networks, the browser itself can become a bridge to services the external requester could not otherwise reach.&lt;/p&gt;

&lt;p&gt;RunOnMine's protected browser path therefore treats destination access as part of the policy boundary.&lt;/p&gt;

&lt;p&gt;The browser uses an isolated profile and protected network behavior. The implementation checks destinations and uses a controlled proxy path for protected automation rather than assuming profile isolation solves private-network access.&lt;/p&gt;

&lt;p&gt;This is also why attaching to an arbitrary existing browser through CDP is a different trust decision: launch-time protections cannot be retroactively applied to a process that was started outside that boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote access without a raw public MCP listener
&lt;/h2&gt;

&lt;p&gt;I did not want "use this remotely" to mean "bind the MCP server to 0.0.0.0".&lt;/p&gt;

&lt;p&gt;RunOnMine keeps its HTTP MCP listener on loopback.&lt;/p&gt;

&lt;p&gt;Remote connectivity is carried outward through supported connector/tunnel models. The current beta supports local stdio, opt-in authenticated loopback HTTP, Cloudflare connector modes and OpenAI Secure MCP Tunnel integration.&lt;/p&gt;

&lt;p&gt;The network transport still does not become the policy authority.&lt;/p&gt;

&lt;p&gt;Remote callers remain subject to local policy. Dangerous remote actions cannot approve themselves, and remote administrator execution is denied by the safety ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emergency Lock is different from uninstall
&lt;/h2&gt;

&lt;p&gt;A system that can perform real machine actions needs a cheap revocation path.&lt;/p&gt;

&lt;p&gt;The owner should not have to delete configuration or dismantle the installation to say "stop now".&lt;/p&gt;

&lt;p&gt;RunOnMine has an Emergency Lock operation for that purpose.&lt;/p&gt;

&lt;p&gt;It stops the active agent/connectors, rejects pending approvals, removes temporary grants, revokes relevant OAuth state and invalidates temporary credentials used by supported connector paths.&lt;/p&gt;

&lt;p&gt;The local configuration remains, so the owner can inspect and recover deliberately later.&lt;/p&gt;

&lt;p&gt;That distinction matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;revocation is a runtime operation, not an uninstall workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit should not become a second secret store
&lt;/h2&gt;

&lt;p&gt;Machine automation needs an audit trail.&lt;/p&gt;

&lt;p&gt;But logging everything naively can create a new security problem.&lt;/p&gt;

&lt;p&gt;Raw command payloads, credentials, URLs and machine paths can contain sensitive data. A debug bundle that simply archives an application state directory is easy to create and difficult to trust.&lt;/p&gt;

&lt;p&gt;RunOnMine keeps a tamper-evident audit chain while avoiding raw secret storage in audit records. Support material is built from bounded, redacted diagnostics instead of blindly copying internal state.&lt;/p&gt;

&lt;p&gt;The audit trail is not meant to make a compromised user account tamper-proof. It is meant to make normal machine actions and unsophisticated modification visible without turning observability into another credential leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release engineering is part of the boundary
&lt;/h2&gt;

&lt;p&gt;Security claims are difficult to reason about when the tested source and the downloadable artifact are ambiguous.&lt;/p&gt;

&lt;p&gt;For the public beta I wanted release evidence tied to an exact candidate.&lt;/p&gt;

&lt;p&gt;The repository records the frozen source candidate and release gates in machine-readable files. Platform acceptance evidence names the source revision and artifact hashes. The release also includes SHA-256 checksums and target-specific CycloneDX SBOMs.&lt;/p&gt;

&lt;p&gt;That does not replace code signing or an external review.&lt;/p&gt;

&lt;p&gt;It makes the beta evidence inspectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the beta does not claim
&lt;/h2&gt;

&lt;p&gt;RunOnMine is prerelease software.&lt;/p&gt;

&lt;p&gt;The current beta has intentionally visible limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS is ad-hoc signed, not Developer ID signed/notarized&lt;/li&gt;
&lt;li&gt;the Windows installer is not Authenticode signed&lt;/li&gt;
&lt;li&gt;an independent external security review is still pending&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those gaps are documented because a security-oriented tool should not hide its own distribution trust limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public beta
&lt;/h2&gt;

&lt;p&gt;RunOnMine &lt;code&gt;v0.1.0-beta.1&lt;/code&gt; is available for macOS, Windows and Linux.&lt;/p&gt;

&lt;p&gt;It is written in Rust and licensed under Apache-2.0.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/ademisler/RunOnMine" rel="noopener noreferrer"&gt;https://github.com/ademisler/RunOnMine&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://runonmine.github.io/" rel="noopener noreferrer"&gt;https://runonmine.github.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main lesson from building it is that giving an agent more tools is the easy part.&lt;/p&gt;

&lt;p&gt;The harder question is deciding where automation ends and owner authority begins.&lt;/p&gt;

&lt;p&gt;For people building or using MCP agents: &lt;strong&gt;which machine actions would you allow automatically, and which would you never allow without an exact local approval?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>rust</category>
      <category>security</category>
    </item>
    <item>
      <title>How Myna Player Generates Context-Aware Subtitles Ahead of Playback</title>
      <dc:creator>Adem İşler</dc:creator>
      <pubDate>Sat, 01 Aug 2026 23:44:40 +0000</pubDate>
      <link>https://dev.to/ademisler/how-myna-player-generates-context-aware-subtitles-ahead-of-playback-4n3i</link>
      <guid>https://dev.to/ademisler/how-myna-player-generates-context-aware-subtitles-ahead-of-playback-4n3i</guid>
      <description>&lt;p&gt;Subtitles have a timing problem that becomes more visible when AI translation enters the pipeline.&lt;/p&gt;

&lt;p&gt;Translate every tiny fragment immediately and the text may arrive on time, but important context disappears. Wait for a longer passage and the translation becomes more coherent, but the subtitle may appear after the dialogue has already passed.&lt;/p&gt;

&lt;p&gt;That creates a practical tradeoff:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;speed or context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted to see whether a media player could avoid making that choice at playback time.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;Myna Player&lt;/strong&gt;, an open-source, local-first AI video player that processes audio ahead of the current playback position, transcribes it locally, translates finalized subtitle cues with surrounding context, and renders them against the native player clock.&lt;/p&gt;

&lt;p&gt;Myna Player is currently a public alpha. The core macOS application works, Windows code and packaging are under active verification, and the first stable public release has not been published yet.&lt;/p&gt;

&lt;p&gt;This article explains the architecture, the subtitle timing contract, and why processing ahead of playback changes the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product boundary
&lt;/h2&gt;

&lt;p&gt;Myna Player is not a general-purpose transcription editor with a video preview attached.&lt;/p&gt;

&lt;p&gt;It is a media player with a just-in-time subtitle pipeline.&lt;/p&gt;

&lt;p&gt;That distinction affects the architecture.&lt;/p&gt;

&lt;p&gt;The system must keep several things true at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;playback remains responsive;&lt;/li&gt;
&lt;li&gt;transcription stays ahead of the viewer;&lt;/li&gt;
&lt;li&gt;seeking does not corrupt old work;&lt;/li&gt;
&lt;li&gt;completed subtitle work can resume later;&lt;/li&gt;
&lt;li&gt;translation receives enough context;&lt;/li&gt;
&lt;li&gt;translated cues keep the original timing;&lt;/li&gt;
&lt;li&gt;local media and audio remain on the device;&lt;/li&gt;
&lt;li&gt;the renderer never waits for an AI request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simplified flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;native player clock
        |
        v
persistent priority scheduler
        |
        v
FFmpeg audio windows
        |
        v
local Whisper worker + VAD
        |
        v
timed source subtitle cues
        |
        +--------------------+
        |                    |
        v                    v
local source cache     optional translation workers
        |                    |
        +----------+---------+
                   |
                   v
       synchronized subtitle renderer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The key idea is that the expensive work happens outside the playback loop.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why listening to system audio would be the wrong abstraction
&lt;/h2&gt;

&lt;p&gt;A live-caption application often listens to whatever audio is currently being played.&lt;/p&gt;

&lt;p&gt;That is useful when the source is unknown or truly live.&lt;/p&gt;

&lt;p&gt;Myna Player works with local media files. It can read the source directly.&lt;/p&gt;

&lt;p&gt;That means it does not need to wait for sound to leave the speakers before analyzing it. It can extract audio from later sections of the file and prepare subtitles before playback reaches them.&lt;/p&gt;

&lt;p&gt;Direct file access provides several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministic time ranges;&lt;/li&gt;
&lt;li&gt;repeatable extraction;&lt;/li&gt;
&lt;li&gt;precise stream selection;&lt;/li&gt;
&lt;li&gt;the ability to jump ahead;&lt;/li&gt;
&lt;li&gt;resumable cached work;&lt;/li&gt;
&lt;li&gt;no dependency on system-audio capture permissions;&lt;/li&gt;
&lt;li&gt;no feedback from notification sounds or other applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The media file itself becomes the source of truth.&lt;/p&gt;
&lt;h2&gt;
  
  
  The processing unit is a canonical time window
&lt;/h2&gt;

&lt;p&gt;Myna Player schedules deterministic, non-overlapping audio windows.&lt;/p&gt;

&lt;p&gt;The current design uses canonical 30-second ranges.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00:00–00:30
00:30–01:00
01:00–01:30
01:30–02:00
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Canonical windows are important because they give the cache stable identities.&lt;/p&gt;

&lt;p&gt;If the same video is reopened, the application can determine exactly which regions were completed. If the user seeks, already finished windows do not have to be transcribed again.&lt;/p&gt;

&lt;p&gt;The scheduler assigns priority according to playback position:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the first required window is urgent;&lt;/li&gt;
&lt;li&gt;roughly the next 90 seconds are promoted;&lt;/li&gt;
&lt;li&gt;later windows remain background work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is not simply “transcribe the entire file as fast as possible.”&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep the region the viewer is about to reach prepared, while using spare capacity to extend the cache farther into the video.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Media fingerprints and pipeline keys
&lt;/h2&gt;

&lt;p&gt;A file path alone is not a safe cache key.&lt;/p&gt;

&lt;p&gt;The media may change. The selected audio stream may change. The speech model, language mode, VAD model, chunk size, or cue-segmentation rules may change.&lt;/p&gt;

&lt;p&gt;Myna Player creates a media fingerprint and combines it with a pipeline key that includes inputs such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selected audio stream;&lt;/li&gt;
&lt;li&gt;Whisper runtime and model hash;&lt;/li&gt;
&lt;li&gt;transcription language mode;&lt;/li&gt;
&lt;li&gt;VAD model;&lt;/li&gt;
&lt;li&gt;chunk and extraction parameters;&lt;/li&gt;
&lt;li&gt;cue-segmentation version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only checkpoints with an exactly matching key are restored.&lt;/p&gt;

&lt;p&gt;This prevents a subtle class of bugs where old subtitle data appears valid even though it was produced by a different pipeline configuration.&lt;/p&gt;

&lt;p&gt;A cache should not merely remember that work happened. It should remember &lt;strong&gt;which work happened under which assumptions&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Seeking requires generations, not only cancellation
&lt;/h2&gt;

&lt;p&gt;A viewer may seek from minute 3 to minute 48 while transcription is processing minute 4.&lt;/p&gt;

&lt;p&gt;The current worker needs to stop, but cancellation alone is not sufficient.&lt;/p&gt;

&lt;p&gt;A slow process may still return after the seek. If its result is accepted blindly, stale cues from the previous playback region can overwrite or pollute the new session.&lt;/p&gt;

&lt;p&gt;Myna Player increments a processing generation when the active region changes.&lt;/p&gt;

&lt;p&gt;The seek flow is conceptually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;cancel the active ASR and translation connection;&lt;/li&gt;
&lt;li&gt;increment the generation;&lt;/li&gt;
&lt;li&gt;preserve completed cache entries;&lt;/li&gt;
&lt;li&gt;promote the newly requested region;&lt;/li&gt;
&lt;li&gt;reject late results from older generations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Generations turn cancellation into a verifiable contract.&lt;/p&gt;

&lt;p&gt;The system does not have to assume that every subprocess stopped instantly. It can recognize that an old result belongs to an obsolete plan.&lt;/p&gt;
&lt;h2&gt;
  
  
  Extracting audio with context
&lt;/h2&gt;

&lt;p&gt;Each canonical window is expanded slightly during extraction.&lt;/p&gt;

&lt;p&gt;Myna Player reads approximately two seconds of neighboring audio context around the canonical range and converts it to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mono
16 kHz
signed 16-bit PCM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The context helps speech recognition around boundaries, but the persisted output still belongs to the canonical time range.&lt;/p&gt;

&lt;p&gt;This is a common pattern in stream processing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read overlapping input for better local decisions;&lt;/li&gt;
&lt;li&gt;write non-overlapping canonical output for deterministic storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without boundary context, words split across windows can be lost or duplicated. Without canonical output boundaries, overlapping windows can produce conflicting cues.&lt;/p&gt;
&lt;h2&gt;
  
  
  A long-lived local Whisper worker
&lt;/h2&gt;

&lt;p&gt;Starting a speech-recognition process for every 30-second segment would add significant overhead.&lt;/p&gt;

&lt;p&gt;Myna Player uses a long-lived local whisper.cpp worker.&lt;/p&gt;

&lt;p&gt;The worker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;binds only to a random &lt;code&gt;127.0.0.1&lt;/code&gt; port;&lt;/li&gt;
&lt;li&gt;loads the selected model once;&lt;/li&gt;
&lt;li&gt;accepts extracted audio windows;&lt;/li&gt;
&lt;li&gt;can apply Silero voice activity detection;&lt;/li&gt;
&lt;li&gt;returns word-level timestamps;&lt;/li&gt;
&lt;li&gt;is terminated on cancellation or application shutdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Word timestamps are essential because the language model should not invent the subtitle timing.&lt;/p&gt;

&lt;p&gt;The transcription layer produces timed words. A deterministic segmentation stage then converts those words into readable, non-overlapping subtitle cues.&lt;/p&gt;

&lt;p&gt;That separation lets the system improve visual subtitle grouping without asking the translation provider to decide when a cue should appear.&lt;/p&gt;
&lt;h2&gt;
  
  
  Voice activity detection reduces wasted work
&lt;/h2&gt;

&lt;p&gt;A 30-second media window may contain music, silence, or non-speech sound.&lt;/p&gt;

&lt;p&gt;Optional Silero VAD identifies speech regions before transcription.&lt;/p&gt;

&lt;p&gt;That can reduce unnecessary inference and avoid producing unstable text from silence or background audio.&lt;/p&gt;

&lt;p&gt;VAD is part of the pipeline fingerprint because changing the speech-region detector can change the resulting transcript and cue boundaries.&lt;/p&gt;

&lt;p&gt;Again, the cache key must represent the actual computation, not only the source file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Persisting source cues transactionally
&lt;/h2&gt;

&lt;p&gt;After a source window is transcribed and segmented, Myna Player stores the source cues and marks the checkpoint complete in the same SQLite transaction.&lt;/p&gt;

&lt;p&gt;This prevents a failure state such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkpoint says complete;&lt;/li&gt;
&lt;li&gt;subtitle rows were only partially written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or the reverse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;subtitle rows exist;&lt;/li&gt;
&lt;li&gt;checkpoint still says incomplete;&lt;/li&gt;
&lt;li&gt;the application repeats the work and creates overlap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The storage layer uses SQLite with WAL mode and versioned migrations.&lt;/p&gt;

&lt;p&gt;The database contains local information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;media fingerprints;&lt;/li&gt;
&lt;li&gt;playback position;&lt;/li&gt;
&lt;li&gt;processing checkpoints;&lt;/li&gt;
&lt;li&gt;source transcript segments;&lt;/li&gt;
&lt;li&gt;provider-specific translations;&lt;/li&gt;
&lt;li&gt;model metadata;&lt;/li&gt;
&lt;li&gt;user corrections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Persistent storage is what turns the look-ahead pipeline from a temporary live effect into resumable product behavior.&lt;/p&gt;
&lt;h2&gt;
  
  
  Transcription and translation are separate workers
&lt;/h2&gt;

&lt;p&gt;Translation should not block source transcription.&lt;/p&gt;

&lt;p&gt;Myna Player runs ASR and cloud translation as independent scheduling lanes.&lt;/p&gt;

&lt;p&gt;The source pipeline can continue preparing timed cues while the translation worker processes bounded groups of completed cues.&lt;/p&gt;

&lt;p&gt;This matters because cloud providers have variable latency. A slow translation response should not stop the application from building the source-language cache.&lt;/p&gt;

&lt;p&gt;It also means a user can switch translation providers or target languages without retranscribing the media.&lt;/p&gt;

&lt;p&gt;Source text and provider-specific translations are stored separately.&lt;/p&gt;

&lt;p&gt;The structure is conceptually:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source cue
  ├── DeepL / French
  ├── Gemini / Turkish
  ├── OpenAI / English
  └── MiniMax / German
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The source timing remains the shared foundation.&lt;/p&gt;
&lt;h2&gt;
  
  
  The translation contract: context may change words, not time
&lt;/h2&gt;

&lt;p&gt;This is the central rule of the translation layer.&lt;/p&gt;

&lt;p&gt;A translation provider receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bounded batch of finalized cues;&lt;/li&gt;
&lt;li&gt;stable cue IDs;&lt;/li&gt;
&lt;li&gt;limited previous-dialogue context;&lt;/li&gt;
&lt;li&gt;the requested target language;&lt;/li&gt;
&lt;li&gt;instructions to return every requested cue exactly once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The provider may improve wording using context.&lt;/p&gt;

&lt;p&gt;It may not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merge cue IDs;&lt;/li&gt;
&lt;li&gt;split one cue into new IDs;&lt;/li&gt;
&lt;li&gt;reorder cues;&lt;/li&gt;
&lt;li&gt;omit cues;&lt;/li&gt;
&lt;li&gt;duplicate cues;&lt;/li&gt;
&lt;li&gt;invent timestamps;&lt;/li&gt;
&lt;li&gt;change the source timing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Responses are validated before persistence.&lt;/p&gt;

&lt;p&gt;Missing, duplicate, unknown, or reordered IDs are rejected.&lt;/p&gt;

&lt;p&gt;This creates a useful division of responsibility:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ASR&lt;/td&gt;
&lt;td&gt;Recognize words and timestamps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cue segmentation&lt;/td&gt;
&lt;td&gt;Create readable timed source units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation&lt;/td&gt;
&lt;td&gt;Produce context-aware text for those units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Player clock&lt;/td&gt;
&lt;td&gt;Decide which cue is visible now&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The language model handles language. Deterministic code handles time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why surrounding context still matters
&lt;/h2&gt;

&lt;p&gt;A subtitle cue is often too small to translate correctly in isolation.&lt;/p&gt;

&lt;p&gt;Pronouns, formality, verb tense, gender, idioms, and ambiguous words may depend on previous dialogue.&lt;/p&gt;

&lt;p&gt;Sending the entire film for every request would be expensive, slow, and unnecessary.&lt;/p&gt;

&lt;p&gt;Myna Player sends bounded prior dialogue context with the active batch.&lt;/p&gt;

&lt;p&gt;The contextual text helps the provider interpret the current lines, but only the requested cue IDs are accepted as output.&lt;/p&gt;

&lt;p&gt;This separates &lt;strong&gt;context for understanding&lt;/strong&gt; from &lt;strong&gt;scope for mutation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The provider can read more than it is allowed to rewrite.&lt;/p&gt;

&lt;p&gt;That is a useful pattern beyond subtitles. AI systems often benefit from broad context while still needing narrow, validated output boundaries.&lt;/p&gt;
&lt;h2&gt;
  
  
  Source edits invalidate translations
&lt;/h2&gt;

&lt;p&gt;Users can correct subtitle text and timing.&lt;/p&gt;

&lt;p&gt;When the source text of a cue changes, every provider translation derived from the old text becomes stale.&lt;/p&gt;

&lt;p&gt;Myna Player invalidates those translations rather than silently continuing to display them.&lt;/p&gt;

&lt;p&gt;Source and translated data therefore have an explicit dependency relationship.&lt;/p&gt;

&lt;p&gt;A correction is not just a visual edit. It changes the input to downstream language processing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Incremental updates over Tauri Channels
&lt;/h2&gt;

&lt;p&gt;The UI does not reload the entire subtitle set every time a worker finishes a window.&lt;/p&gt;

&lt;p&gt;The backend sends:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;one full initial snapshot;&lt;/li&gt;
&lt;li&gt;ordered incremental cue patches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These updates travel through Tauri Channels.&lt;/p&gt;

&lt;p&gt;Ordering matters because source cues, translations, corrections, and invalidations may arrive from different workers.&lt;/p&gt;

&lt;p&gt;The frontend applies a known sequence of patches instead of trying to reconstruct state from unordered events.&lt;/p&gt;

&lt;p&gt;This keeps the WebView focused on presentation while the native backend owns persistence and pipeline state.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rendering against a native player clock
&lt;/h2&gt;

&lt;p&gt;The subtitle renderer does not call Whisper or a translation API.&lt;/p&gt;

&lt;p&gt;It reads already cached cues and compares them to a player clock updated at roughly 100 ms intervals.&lt;/p&gt;

&lt;p&gt;Cue lookup uses binary search rather than scanning every subtitle on every tick.&lt;/p&gt;

&lt;p&gt;This provides a strict runtime rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Playback rendering must remain deterministic even when background AI work is slow, offline, or failing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the next translated cue is not ready, the system can display source text or wait according to the selected mode. It does not freeze playback while waiting for a model.&lt;/p&gt;
&lt;h2&gt;
  
  
  Native video below a transparent WebView
&lt;/h2&gt;

&lt;p&gt;Myna Player uses Tauri 2 and a Leptos interface compiled to WebAssembly, but video rendering is native.&lt;/p&gt;

&lt;p&gt;On macOS, libVLC renders into an application-owned &lt;code&gt;NSView&lt;/code&gt; positioned below the transparent Tauri WebView.&lt;/p&gt;

&lt;p&gt;The web layer owns controls and generated subtitles. AppKit owns the video surface geometry and updates it during resize and fullscreen transitions.&lt;/p&gt;

&lt;p&gt;On Windows, the application creates a child HWND below WebView2 and binds libVLC to that surface.&lt;/p&gt;

&lt;p&gt;This composition avoids forcing decoded video frames through the web interface.&lt;/p&gt;

&lt;p&gt;It also creates a clear boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;native layer: playback, clock, surfaces, media tracks;&lt;/li&gt;
&lt;li&gt;web UI: controls, settings, subtitles, progress state;&lt;/li&gt;
&lt;li&gt;Rust backend: scheduling, storage, processing, providers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unsafe libVLC interaction remains isolated inside a limited player crate.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Rust workspace follows product boundaries
&lt;/h2&gt;

&lt;p&gt;The repository is split into focused crates:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myna-player-core       domain models and scheduling
myna-player-media      FFprobe and FFmpeg integration
myna-player-player     native libVLC engine
myna-player-jobs       persistent priorities, retry, resume
myna-player-storage    SQLite/WAL and media fingerprints
myna-player-pipeline   ASR, VAD, translation, cue processing
myna-player-providers  provider registry and credentials
src-tauri              desktop composition and IPC
myna-player-ui         Leptos/WASM interface
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The separation is not only organizational.&lt;/p&gt;

&lt;p&gt;It helps keep several risky concerns from leaking into each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shell process execution stays in media and pipeline boundaries;&lt;/li&gt;
&lt;li&gt;unsafe player calls stay in the player layer;&lt;/li&gt;
&lt;li&gt;credentials stay behind the provider boundary;&lt;/li&gt;
&lt;li&gt;persistent scheduling stays independent from UI state;&lt;/li&gt;
&lt;li&gt;serializable domain types remain usable in tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Local-first privacy
&lt;/h2&gt;

&lt;p&gt;The following operations run on the user's computer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FFprobe media inspection;&lt;/li&gt;
&lt;li&gt;FFmpeg audio extraction;&lt;/li&gt;
&lt;li&gt;voice activity detection;&lt;/li&gt;
&lt;li&gt;whisper.cpp speech recognition;&lt;/li&gt;
&lt;li&gt;subtitle timing and segmentation;&lt;/li&gt;
&lt;li&gt;playback;&lt;/li&gt;
&lt;li&gt;editing and export;&lt;/li&gt;
&lt;li&gt;caching and checkpoint management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloud translation is opt-in.&lt;/p&gt;

&lt;p&gt;When enabled, the provider receives finalized transcript text and limited neighboring context. It does not receive the video file or extracted audio.&lt;/p&gt;

&lt;p&gt;Provider credentials are stored through the operating system credential store and are never returned to the web UI.&lt;/p&gt;

&lt;p&gt;The local Whisper worker binds only to localhost, and diagnostic logs are bounded and redact home-directory paths.&lt;/p&gt;

&lt;p&gt;Local-first does not mean every optional feature is offline. It means the user can understand and control which data crosses the device boundary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Per-video deletion
&lt;/h2&gt;

&lt;p&gt;Generated data is grouped under the media fingerprint.&lt;/p&gt;

&lt;p&gt;SQLite foreign keys use cascading deletion so a video can be reset atomically from the application's point of view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reset this video&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;cancels the active ASR and translation generation;&lt;/li&gt;
&lt;li&gt;pauses playback and seeks to the beginning;&lt;/li&gt;
&lt;li&gt;deletes checkpoints, transcripts, translations, cache, and remembered position;&lt;/li&gt;
&lt;li&gt;recreates only the open-media identity;&lt;/li&gt;
&lt;li&gt;leaves global settings, installed models, and credentials intact.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is another place where product wording matters.&lt;/p&gt;

&lt;p&gt;“Clear cache” is too vague. A user should know whether a reset deletes subtitle history, corrections, translations, or provider credentials.&lt;/p&gt;
&lt;h2&gt;
  
  
  Packaging AI runtimes is part of the application
&lt;/h2&gt;

&lt;p&gt;A local-first desktop AI product is not complete when it builds on the developer's machine.&lt;/p&gt;

&lt;p&gt;Myna Player needs native runtimes and models such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VLC and plugins;&lt;/li&gt;
&lt;li&gt;FFmpeg and FFprobe;&lt;/li&gt;
&lt;li&gt;whisper.cpp;&lt;/li&gt;
&lt;li&gt;optional Silero VAD data;&lt;/li&gt;
&lt;li&gt;Whisper model files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The packaging scripts download or build pinned components, verify SHA-256 checksums, stage required licenses, and create native bundles.&lt;/p&gt;

&lt;p&gt;Models are activated only after expected size and checksum verification.&lt;/p&gt;

&lt;p&gt;The release workflow is designed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build on clean macOS and Windows runners;&lt;/li&gt;
&lt;li&gt;verify packaged runtimes;&lt;/li&gt;
&lt;li&gt;exercise native playback smoke tests;&lt;/li&gt;
&lt;li&gt;sign nested binaries;&lt;/li&gt;
&lt;li&gt;notarize macOS artifacts;&lt;/li&gt;
&lt;li&gt;verify Windows signatures;&lt;/li&gt;
&lt;li&gt;publish checksums.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is not marked release-verified until those workflows pass with the required signing configuration.&lt;/p&gt;

&lt;p&gt;That conservative status is intentional. A successful development build is not the same as a trustworthy public installer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Export and interoperability
&lt;/h2&gt;

&lt;p&gt;Myna Player can export:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source subtitles;&lt;/li&gt;
&lt;li&gt;translated subtitles;&lt;/li&gt;
&lt;li&gt;dual-language subtitles;&lt;/li&gt;
&lt;li&gt;SRT;&lt;/li&gt;
&lt;li&gt;VTT.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generated subtitles are not trapped inside the application.&lt;/p&gt;

&lt;p&gt;Export is useful for users, but it is also a debugging tool. A persisted subtitle file makes it easier to inspect cue boundaries, timing drift, translation consistency, and corrections outside the player.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Processing ahead changes the latency budget
&lt;/h3&gt;

&lt;p&gt;The system does not need every AI operation to be faster than human speech. It needs the prepared region to remain ahead of the playback clock.&lt;/p&gt;

&lt;p&gt;That turns the problem from instantaneous inference into scheduling.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Context and timing should belong to different layers
&lt;/h3&gt;

&lt;p&gt;Translation needs surrounding dialogue. Timing should remain deterministic. Stable cue IDs create the boundary between them.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Cancellation needs versioned state
&lt;/h3&gt;

&lt;p&gt;Generation numbers make late worker results rejectable. Without them, a seek can create subtle stale-data races.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Cache validity depends on the complete pipeline
&lt;/h3&gt;

&lt;p&gt;A transcript produced with another model, language mode, VAD configuration, or segmentation version is not necessarily reusable.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. AI must not enter the render loop
&lt;/h3&gt;

&lt;p&gt;The player should remain responsive even when models are slow, credentials are missing, or the network is unavailable.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Packaging is an engineering domain of its own
&lt;/h3&gt;

&lt;p&gt;Bundling VLC, FFmpeg, Whisper, models, licenses, signatures, and platform-specific surfaces required as much architectural attention as the subtitle pipeline.&lt;/p&gt;
&lt;h3&gt;
  
  
  7. Local-first is a data-flow promise
&lt;/h3&gt;

&lt;p&gt;It is not enough to say “privacy focused.” The product must document which operations are local, what optional text leaves the device, where credentials live, and how generated data can be deleted.&lt;/p&gt;
&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;Myna Player is open source under GPL-3.0-or-later and remains in public alpha.&lt;/p&gt;

&lt;p&gt;The core macOS experience is integrated. Windows playback and packaging code exists and is being verified through clean-runner workflows. Signed public alpha artifacts and broader clean-machine smoke tests remain release milestones.&lt;/p&gt;

&lt;p&gt;The repository includes architecture, privacy, security, contribution, packaging, release, and third-party licensing documentation.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ademisler" rel="noopener noreferrer"&gt;
        ademisler
      &lt;/a&gt; / &lt;a href="https://github.com/ademisler/Myna-Player" rel="noopener noreferrer"&gt;
        Myna-Player
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Open-source, local-first AI video player for ahead-of-playback transcription, contextual translation, and precisely timed subtitles.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a href="https://myna-player.github.io/" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fademisler%2FMyna-Player%2FHEAD%2Fmyna_player_icon.svg" alt="Myna Player" width="132" height="132"&gt;
  &lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Myna Player&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Watch in any language.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;
  A local-first, open-source AI video player that transcribes ahead, translates with context, and displays each subtitle at the right moment
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://myna-player.github.io/" rel="nofollow noopener noreferrer"&gt;Website&lt;/a&gt; ·
  &lt;a href="https://github.com/ademisler/Myna-Player/docs/architecture.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; ·
  &lt;a href="https://github.com/ademisler/Myna-Player/docs/roadmap.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
  &lt;a href="https://github.com/ademisler/Myna-Player/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
  &lt;a href="https://github.com/ademisler/Myna-Player/SECURITY.md" rel="noopener noreferrer"&gt;Security&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/ademisler/Myna-Player/actions/workflows/quality.yml" rel="noopener noreferrer"&gt;&lt;img alt="Quality" src="https://github.com/ademisler/Myna-Player/actions/workflows/quality.yml/badge.svg"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/ademisler/Myna-Player/LICENSE" rel="noopener noreferrer"&gt;&lt;img alt="License: GPL-3.0-or-later" src="https://camo.githubusercontent.com/569456ec914750583cbe7487d29ecfb81c5e6132151d3ed251cd78ea7307c18b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d47504c2d2d332e302d2d6f722d2d6c617465722d663062353162"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/adf30125ecf506aee693db4d3dc9c8de1bb7d93eb9e0cb83675f2f0b488676fb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374617475732d7075626c6963253230616c7068612d313731393138"&gt;&lt;img alt="Status: public alpha" src="https://camo.githubusercontent.com/adf30125ecf506aee693db4d3dc9c8de1bb7d93eb9e0cb83675f2f0b488676fb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374617475732d7075626c6963253230616c7068612d313731393138"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/7afd7dce865902dfef434b2326d084ba4ebe866c82fe4c0fba0bf70e91a17926/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5275737425323025324225323054617572692d326232643262"&gt;&lt;img alt="Rust and Tauri" src="https://camo.githubusercontent.com/7afd7dce865902dfef434b2326d084ba4ebe866c82fe4c0fba0bf70e91a17926/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5275737425323025324225323054617572692d326232643262"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Project status&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Myna Player is a &lt;strong&gt;public alpha&lt;/strong&gt;. The core macOS application works, Windows code and packaging are under active verification, and the first stable public release has not been published yet. Expect interface, storage, and packaging changes before 1.0.&lt;/p&gt;

&lt;p&gt;The repository is open for review and contribution. Please read &lt;a href="https://github.com/ademisler/Myna-Player/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; before proposing substantial changes.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why Myna Player exists&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Live subtitle systems often choose between speed and coherence. Translating tiny fragments quickly loses context; translating long blocks makes subtitles arrive late. Myna Player separates those concerns:&lt;/p&gt;


&lt;ol&gt;

&lt;li&gt;it processes audio ahead of playback;&lt;/li&gt;

&lt;li&gt;Whisper produces word-level timing locally;&lt;/li&gt;

&lt;li&gt;natural subtitle cues are derived from those timestamps;&lt;/li&gt;

&lt;li&gt;translation sees surrounding context but may not rewrite cue…&lt;/li&gt;

&lt;/ol&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ademisler/Myna-Player" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;I would especially value feedback on these areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How far ahead of playback should a subtitle pipeline normally process?&lt;/li&gt;
&lt;li&gt;Which subtitle segmentation rules produce the best balance between readability and faithful timing?&lt;/li&gt;
&lt;li&gt;Should contextual translation include only previous dialogue, or a limited amount of future dialogue as well?&lt;/li&gt;
&lt;li&gt;What should a player display when source subtitles are ready but translation is still pending?&lt;/li&gt;
&lt;li&gt;Which clean-machine tests would make you trust an alpha desktop AI application?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the fifth and final article in this first series documenting the products I have built—from a browser toolbox to local AI desktop systems.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>opensource</category>
      <category>tauri</category>
    </item>
    <item>
      <title>Building a Local-First Quota Monitor and Account Switcher for Codex</title>
      <dc:creator>Adem İşler</dc:creator>
      <pubDate>Sat, 01 Aug 2026 23:42:39 +0000</pubDate>
      <link>https://dev.to/ademisler/building-a-local-first-quota-monitor-and-account-switcher-for-codex-379h</link>
      <guid>https://dev.to/ademisler/building-a-local-first-quota-monitor-and-account-switcher-for-codex-379h</guid>
      <description>&lt;p&gt;Codex account management looks simple until several accounts have different quota windows, different reset times, and different local session states.&lt;/p&gt;

&lt;p&gt;At that point, two questions become surprisingly difficult:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which account is actually usable right now?&lt;/li&gt;
&lt;li&gt;Which account is the local Codex installation currently using?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I originally wanted a small menu-bar utility that could answer those questions without opening dashboards, estimating usage from local activity, or manually replacing authentication files.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;CodexControl&lt;/strong&gt;: an open-source, local-first quota monitor and account switcher for Codex on macOS and Windows.&lt;/p&gt;

&lt;p&gt;The product intentionally does only two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read live Codex quota state;&lt;/li&gt;
&lt;li&gt;switch the active local Codex account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping the scope narrow made the interface simple. The implementation, especially cross-platform switching, was less simple than it first appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a quota monitor needs live data
&lt;/h2&gt;

&lt;p&gt;A usage estimate can be useful when an API exposes no authoritative quota information.&lt;/p&gt;

&lt;p&gt;Codex already has account state that can be read through the authenticated local session, so I did not want to infer remaining capacity from token counts, request logs, or time spent working.&lt;/p&gt;

&lt;p&gt;CodexControl uses each saved account's local authentication state to request live quota information directly.&lt;/p&gt;

&lt;p&gt;That matters because an estimate can drift in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;usage may happen from another computer;&lt;/li&gt;
&lt;li&gt;background activity may not be visible locally;&lt;/li&gt;
&lt;li&gt;different models or operations may consume capacity differently;&lt;/li&gt;
&lt;li&gt;the server may adjust or normalize quota state;&lt;/li&gt;
&lt;li&gt;cached responses may remain visible after the real window changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app treats the provider response as the source of truth instead of trying to reconstruct it indirectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quota is not a single percentage
&lt;/h2&gt;

&lt;p&gt;A Codex account may expose more than one usage window.&lt;/p&gt;

&lt;p&gt;The two windows most important to the app are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a shorter five-hour window;&lt;/li&gt;
&lt;li&gt;a longer seven-day window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These windows can have very different states.&lt;/p&gt;

&lt;p&gt;An account may still have weekly capacity while the short-term window is exhausted. Another account may have a nearly full short-term window but little weekly capacity remaining.&lt;/p&gt;

&lt;p&gt;Collapsing both into one number would hide the operational decision the user needs to make.&lt;/p&gt;

&lt;p&gt;CodexControl therefore keeps each window independent and shows its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remaining or used capacity;&lt;/li&gt;
&lt;li&gt;reset time;&lt;/li&gt;
&lt;li&gt;current availability;&lt;/li&gt;
&lt;li&gt;normalized presentation state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app also preserves exact reset timestamps rather than displaying only a vague label such as “resets later.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Accuracy became a first-order requirement
&lt;/h2&gt;

&lt;p&gt;A quota monitor loses its value quickly if the numbers cannot be trusted.&lt;/p&gt;

&lt;p&gt;I added several safeguards around live reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cache-bypassing network sessions;&lt;/li&gt;
&lt;li&gt;repeated reads with equivalence checks;&lt;/li&gt;
&lt;li&gt;rejection of inconsistent responses;&lt;/li&gt;
&lt;li&gt;per-window normalization without merging independent values;&lt;/li&gt;
&lt;li&gt;clearing stale snapshots after failed refreshes;&lt;/li&gt;
&lt;li&gt;token refresh before retrying when required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important principle was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A missing value is better than a confident but stale value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If a refresh fails, continuing to show an old quota without a clear stale state can lead the user to switch to an account that is no longer usable.&lt;/p&gt;

&lt;p&gt;The app prefers to expose the failure and request another refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  The account list is sorted for action, not decoration
&lt;/h2&gt;

&lt;p&gt;A raw account list sorted alphabetically does not answer the main question.&lt;/p&gt;

&lt;p&gt;The user is usually trying to decide which account to activate next.&lt;/p&gt;

&lt;p&gt;CodexControl sorts accounts by practical usefulness so that currently usable accounts remain easier to reach. Presentation logic considers live window state, authentication health, and whether the account is active.&lt;/p&gt;

&lt;p&gt;The list also supports local management actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add an account;&lt;/li&gt;
&lt;li&gt;refresh quota;&lt;/li&gt;
&lt;li&gt;reauthenticate;&lt;/li&gt;
&lt;li&gt;relabel;&lt;/li&gt;
&lt;li&gt;remove;&lt;/li&gt;
&lt;li&gt;open the account directory;&lt;/li&gt;
&lt;li&gt;switch the ambient Codex identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Periodic refresh runs every few minutes, but manual refresh remains available when the user needs an immediate answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local accounts are stored as separate Codex homes
&lt;/h2&gt;

&lt;p&gt;Each managed account has its own local Codex home and authentication state.&lt;/p&gt;

&lt;p&gt;The ambient &lt;code&gt;~/.codex&lt;/code&gt; directory represents the account currently used by the normal Codex CLI and desktop workflow.&lt;/p&gt;

&lt;p&gt;A simplified model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;managed account A home ─┐
managed account B home ─┼─&amp;gt; CodexControl
managed account C home ─┘
                             |
                             v
                      ambient ~/.codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Switching means making the selected managed account become the ambient identity.&lt;/p&gt;

&lt;p&gt;The exact work required depends on the platform and the current Codex Desktop implementation.&lt;/p&gt;
&lt;h2&gt;
  
  
  macOS: a native menu-bar workflow
&lt;/h2&gt;

&lt;p&gt;The macOS application is built with SwiftUI and AppKit and lives in the menu bar.&lt;/p&gt;

&lt;p&gt;That form factor matches the task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;glance at quota;&lt;/li&gt;
&lt;li&gt;choose an account;&lt;/li&gt;
&lt;li&gt;switch;&lt;/li&gt;
&lt;li&gt;return to work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no need for a large always-open dashboard.&lt;/p&gt;

&lt;p&gt;On macOS, the app reads local Codex homes, refreshes account tokens when needed, fetches live quota, replaces the ambient session during a switch, and restarts Codex Desktop so the new identity is applied.&lt;/p&gt;

&lt;p&gt;The project includes packaging, signing, notarization, and Homebrew cask support so the utility can behave like a normal desktop application rather than a script collection.&lt;/p&gt;
&lt;h2&gt;
  
  
  Windows revealed that switching is not one file copy
&lt;/h2&gt;

&lt;p&gt;The first Windows implementation switched accounts by replacing only:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.codex/auth.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That was not enough for current Codex Desktop builds.&lt;/p&gt;

&lt;p&gt;The CLI identity could change while the desktop application continued to show a login screen or a stale session.&lt;/p&gt;

&lt;p&gt;The reason was that identity existed in multiple state layers.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 1: CLI and global Codex state
&lt;/h3&gt;

&lt;p&gt;The local Codex directory included:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth.json
.codex-global-state.json
.codex-global-state.json.bak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The global-state files could retain a previous &lt;code&gt;creator_id&lt;/code&gt; even after the authentication file changed.&lt;/p&gt;

&lt;p&gt;The switch flow therefore needed to rewrite the previous provider account ID with the target account ID in both global-state files.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 2: Codex Desktop MSIX session state
&lt;/h3&gt;

&lt;p&gt;The Windows desktop app also maintained browser and application session state inside its MSIX package cache:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%LOCALAPPDATA%\Packages\OpenAI.Codex_*\LocalCache\Roaming\Codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Updating the CLI files did not automatically replace this desktop session layer.&lt;/p&gt;

&lt;p&gt;The visible symptoms were confusing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the switch appeared to complete, but the desktop app still displayed the previous identity;&lt;/li&gt;
&lt;li&gt;the desktop app reopened at a login screen;&lt;/li&gt;
&lt;li&gt;the app terminated but failed to relaunch after a copy error;&lt;/li&gt;
&lt;li&gt;a target account had valid CLI authentication but no matching desktop session yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Backing up desktop session state per account
&lt;/h2&gt;

&lt;p&gt;The Windows restart flow now treats desktop session state as account-specific data.&lt;/p&gt;

&lt;p&gt;During a switch it can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;stop Codex Desktop;&lt;/li&gt;
&lt;li&gt;back up the current desktop session into the managed home of the account being left;&lt;/li&gt;
&lt;li&gt;update &lt;code&gt;auth.json&lt;/code&gt; and global identity state;&lt;/li&gt;
&lt;li&gt;restore the target account's saved desktop session when available;&lt;/li&gt;
&lt;li&gt;relaunch Codex Desktop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The backup and restore process operates per entry with logging rather than failing the entire switch on the first copy problem.&lt;/p&gt;

&lt;p&gt;There is also an important first-use case.&lt;/p&gt;

&lt;p&gt;A target account may not yet have a saved desktop-session snapshot. On the first switch, Codex Desktop may need to reconcile that account's state itself. Once the account has been active and its session has been backed up, later switches can restore it more consistently.&lt;/p&gt;

&lt;p&gt;This was the clearest engineering lesson in the project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Authentication state and application session state are related, but they are not the same thing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Account identity cannot rely on email alone
&lt;/h2&gt;

&lt;p&gt;Another subtle issue appeared when accounts shared identifying fields.&lt;/p&gt;

&lt;p&gt;Using only email or authentication subject as the account key can collapse distinct provider accounts into one row.&lt;/p&gt;

&lt;p&gt;CodexControl prefers the provider account ID and uses other fields only as fallbacks.&lt;/p&gt;

&lt;p&gt;This matters when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple accounts use the same email address;&lt;/li&gt;
&lt;li&gt;an organization changes account metadata;&lt;/li&gt;
&lt;li&gt;authentication records are rotated;&lt;/li&gt;
&lt;li&gt;old and new managed homes coexist temporarily.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stable identity should come from the most provider-specific identifier available.&lt;/p&gt;
&lt;h2&gt;
  
  
  Removed accounts should stay removed
&lt;/h2&gt;

&lt;p&gt;The app discovers managed Codex homes from disk.&lt;/p&gt;

&lt;p&gt;That creates a lifecycle problem: deleting an account from the visible list is not enough if an old duplicate home is rediscovered at the next startup.&lt;/p&gt;

&lt;p&gt;The removal flow therefore needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;record the removed identity;&lt;/li&gt;
&lt;li&gt;filter later discovery against removed identities;&lt;/li&gt;
&lt;li&gt;remove duplicate managed homes for the same provider account;&lt;/li&gt;
&lt;li&gt;avoid recreating an account from stale authentication data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deletion is a state-management feature, not merely a UI action.&lt;/p&gt;
&lt;h2&gt;
  
  
  Recovering from rotated refresh tokens
&lt;/h2&gt;

&lt;p&gt;A user may have multiple local auth homes for the same account, and older homes may contain stale refresh tokens.&lt;/p&gt;

&lt;p&gt;A failed refresh does not always mean the account itself is invalid. A newer matching home may contain the current credentials.&lt;/p&gt;

&lt;p&gt;The recovery path can search matching local identities and use fresher authentication state before declaring that reauthentication is required.&lt;/p&gt;

&lt;p&gt;This avoids forcing the user through login again when valid local credentials already exist.&lt;/p&gt;

&lt;p&gt;At the same time, the app stops automatic retry loops for accounts already known to require reauthentication. Manual refresh and reauthentication remain explicit actions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cross-platform implementation without pretending the platforms are identical
&lt;/h2&gt;

&lt;p&gt;The project uses different native approaches on each platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;SwiftUI and AppKit menu-bar application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;Python tray-first desktop application&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It would have been possible to force both platforms into one cross-platform UI framework.&lt;/p&gt;

&lt;p&gt;I chose not to make shared UI code a goal by itself.&lt;/p&gt;

&lt;p&gt;The shared product behavior is more important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;account discovery;&lt;/li&gt;
&lt;li&gt;identity normalization;&lt;/li&gt;
&lt;li&gt;live quota reads;&lt;/li&gt;
&lt;li&gt;token refresh;&lt;/li&gt;
&lt;li&gt;sorting and presentation rules;&lt;/li&gt;
&lt;li&gt;switching semantics;&lt;/li&gt;
&lt;li&gt;synthetic demo data;&lt;/li&gt;
&lt;li&gt;repository hygiene.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Platform-specific session handling remains platform-specific.&lt;/p&gt;

&lt;p&gt;This is especially important on Windows, where MSIX package state and restart behavior require code that has no meaningful macOS equivalent.&lt;/p&gt;
&lt;h2&gt;
  
  
  Local-first does not mean offline
&lt;/h2&gt;

&lt;p&gt;CodexControl is local-first, but it still contacts OpenAI to refresh authenticated quota information.&lt;/p&gt;

&lt;p&gt;The distinction is about ownership and storage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;account files stay on the user's computer;&lt;/li&gt;
&lt;li&gt;the app reads existing local Codex authentication state;&lt;/li&gt;
&lt;li&gt;there is no CodexControl cloud account database;&lt;/li&gt;
&lt;li&gt;screenshots and public examples use synthetic identities;&lt;/li&gt;
&lt;li&gt;live tokens, snapshots, and desktop sessions are never intended for the repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application stores its own local state under the normal platform application-data directories.&lt;/p&gt;

&lt;p&gt;Credentials and session data deserve more careful treatment than ordinary preferences. The repository security policy explicitly tells users not to include &lt;code&gt;auth.json&lt;/code&gt;, tokens, snapshots, private paths, or real account screenshots in public reports.&lt;/p&gt;
&lt;h2&gt;
  
  
  Synthetic demo data is part of security design
&lt;/h2&gt;

&lt;p&gt;A quota dashboard naturally displays account names, emails, plan details, reset times, and usage values.&lt;/p&gt;

&lt;p&gt;Real screenshots can leak more than expected.&lt;/p&gt;

&lt;p&gt;The repository therefore uses synthetic accounts and snapshots for documentation and product images.&lt;/p&gt;

&lt;p&gt;This is not just a marketing concern. Safe demo modes make it possible to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reproduce UI states;&lt;/li&gt;
&lt;li&gt;test sorting and warning behavior;&lt;/li&gt;
&lt;li&gt;capture screenshots consistently;&lt;/li&gt;
&lt;li&gt;review the project publicly;&lt;/li&gt;
&lt;li&gt;avoid manually redacting every image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reliable synthetic-data path is often safer than asking contributors to “be careful” each time they take a screenshot.&lt;/p&gt;
&lt;h2&gt;
  
  
  Release engineering
&lt;/h2&gt;

&lt;p&gt;CodexControl includes a tag-driven release workflow for both platforms.&lt;/p&gt;

&lt;p&gt;The macOS path can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;import a Developer ID certificate when configured;&lt;/li&gt;
&lt;li&gt;build the app;&lt;/li&gt;
&lt;li&gt;sign the bundle;&lt;/li&gt;
&lt;li&gt;notarize artifacts;&lt;/li&gt;
&lt;li&gt;publish a ZIP and checksum.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Windows path builds a standalone package and publishes its archive and checksum to the same GitHub release.&lt;/p&gt;

&lt;p&gt;The project also includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a static product website;&lt;/li&gt;
&lt;li&gt;direct release downloads;&lt;/li&gt;
&lt;li&gt;a Homebrew cask;&lt;/li&gt;
&lt;li&gt;a changelog;&lt;/li&gt;
&lt;li&gt;platform-specific build and install scripts;&lt;/li&gt;
&lt;li&gt;automated tests for account management and presentation behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Distribution work is part of the product. A quota switcher that only runs from a development checkout would solve a much smaller problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. A narrow product can still require deep systems work
&lt;/h3&gt;

&lt;p&gt;The interface answers two questions, but doing so reliably required authentication handling, live network reads, local state discovery, process restarts, Windows package caches, and release automation.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Accuracy is a user-experience feature
&lt;/h3&gt;

&lt;p&gt;Repeated reads, stale-state clearing, and independent quota windows are not backend details. They determine whether the user trusts the next account-selection decision.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Account switching is distributed state synchronization
&lt;/h3&gt;

&lt;p&gt;The active identity may exist in authentication files, global state, desktop caches, and running processes. Updating one layer does not guarantee the product has switched.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Cross-platform does not require identical implementations
&lt;/h3&gt;

&lt;p&gt;A shared product contract can be more valuable than a shared UI framework. Native platform behavior should remain explicit where the operating systems differ.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Removal and recovery paths deserve first-class design
&lt;/h3&gt;

&lt;p&gt;Adding an account is the easy path. Duplicate discovery, stale refresh tokens, deleted identities, failed restarts, and first-time desktop sessions are where the product becomes dependable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Current scope
&lt;/h2&gt;

&lt;p&gt;CodexControl remains intentionally Codex-specific.&lt;/p&gt;

&lt;p&gt;It is not a general multi-provider dashboard, billing platform, or browser-based account manager.&lt;/p&gt;

&lt;p&gt;It is for people who actively use multiple Codex accounts and need a fast local answer to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which account has usable capacity;&lt;/li&gt;
&lt;li&gt;when its quota resets;&lt;/li&gt;
&lt;li&gt;which account is currently active;&lt;/li&gt;
&lt;li&gt;how to switch without manually repairing local state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is open source and supports macOS and Windows.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ademisler" rel="noopener noreferrer"&gt;
        ademisler
      &lt;/a&gt; / &lt;a href="https://github.com/ademisler/codexcontrol" rel="noopener noreferrer"&gt;
        codexcontrol
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Local-first Codex quota tracker and multi-account switcher for macOS and Windows
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CodexControl&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/ademisler/codexcontrol/./site/assets/codexcontrol-mark.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fademisler%2Fcodexcontrol%2FHEAD%2F.%2Fsite%2Fassets%2Fcodexcontrol-mark.svg" alt="CodexControl logo" width="72"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/ademisler/codexcontrol/./docs/images/codexcontrol-demo-ui.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fademisler%2Fcodexcontrol%2FHEAD%2F.%2Fdocs%2Fimages%2Fcodexcontrol-demo-ui.png" alt="CodexControl demo UI" width="430"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;strong&gt;Local-first Codex quota tracking and account switching for macOS and Windows.&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://codexcontrol.app" rel="nofollow noopener noreferrer"&gt;Website&lt;/a&gt;
  ·
  &lt;a href="https://github.com/ademisler/codexcontrol" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
  ·
  &lt;a href="https://github.com/ademisler/codexcontrol/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt;
  ·
  &lt;a href="https://github.com/ademisler/codexcontrol/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;Security&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8f87985ad8d82ad1e3b016c32248ecb2db763d4af7c7d83cb7bae114ef88f1c1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61634f532d31342532422d3131313131313f6c6f676f3d6170706c65266c6f676f436f6c6f723d7768697465"&gt;&lt;img alt="macOS 14+" src="https://camo.githubusercontent.com/8f87985ad8d82ad1e3b016c32248ecb2db763d4af7c7d83cb7bae114ef88f1c1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61634f532d31342532422d3131313131313f6c6f676f3d6170706c65266c6f676f436f6c6f723d7768697465"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/0c491eac037b3de6484176c23d0856b696529425d015550dd33b88efda8893f5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f57696e646f77732d737570706f727465642d3131313131313f6c6f676f3d77696e646f7773266c6f676f436f6c6f723d7768697465"&gt;&lt;img alt="Windows" src="https://camo.githubusercontent.com/0c491eac037b3de6484176c23d0856b696529425d015550dd33b88efda8893f5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f57696e646f77732d737570706f727465642d3131313131313f6c6f676f3d77696e646f7773266c6f676f436f6c6f723d7768697465"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/49ab1b513efc46e83756ed380b6f1ae65a8976835bea77779098665cb41d2ca5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73746f726167652d6c6f63616c2d2d66697273742d313131313131"&gt;&lt;img alt="Local-first" src="https://camo.githubusercontent.com/49ab1b513efc46e83756ed380b6f1ae65a8976835bea77779098665cb41d2ca5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73746f726167652d6c6f63616c2d2d66697273742d313131313131"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/69ddf80bd59ce4919aab829d1bdf4c9d774194215cdbe6afd8b173346b511ce3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d313131313131"&gt;&lt;img alt="MIT License" src="https://camo.githubusercontent.com/69ddf80bd59ce4919aab829d1bdf4c9d774194215cdbe6afd8b173346b511ce3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d313131313131"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;CodexControl is a focused desktop app for people who actively manage multiple OpenAI Codex accounts.&lt;/p&gt;

&lt;p&gt;It does two things well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shows live quota directly from OpenAI&lt;/li&gt;
&lt;li&gt;switches the active Codex account used by the local CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Homebrew&lt;/h3&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;brew install --cask ademisler/tap/codexcontrol&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Direct Download&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;macOS and Windows release: &lt;a href="https://github.com/ademisler/codexcontrol/releases/latest" rel="noopener noreferrer"&gt;Latest GitHub release&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Homebrew tap: &lt;a href="https://github.com/ademisler/homebrew-tap" rel="noopener noreferrer"&gt;ademisler/homebrew-tap&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why CodexControl&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Most tools around this workflow fall into one of these categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-provider dashboards with too much surface area&lt;/li&gt;
&lt;li&gt;browser-driven quota trackers with extra runtime overhead&lt;/li&gt;
&lt;li&gt;scripts that estimate usage instead of reading the live account state&lt;/li&gt;
&lt;li&gt;switchers that do not tell you which account is actually usable right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CodexControl is intentionally narrower:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Codex-only&lt;/li&gt;
&lt;li&gt;local-first&lt;/li&gt;
&lt;li&gt;fast to scan&lt;/li&gt;
&lt;li&gt;built around real quota windows and active-account control&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Core Capabilities&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;Capability&lt;/th&gt;

&lt;th&gt;What it does&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;Live quota reads&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;…&lt;p&gt;&lt;/p&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ademisler/codexcontrol" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;I would be interested in feedback on a few areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which quota signals are most useful when choosing between accounts?&lt;/li&gt;
&lt;li&gt;How should the UI represent partial availability across short and long windows?&lt;/li&gt;
&lt;li&gt;Have you encountered other desktop apps where authentication and browser-session state diverge?&lt;/li&gt;
&lt;li&gt;Which local-first account-management workflows deserve better tooling?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the fourth article in my series documenting the products I have built. The next one covers Myna Player and the problem of generating context-aware subtitles without letting them arrive late.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>swift</category>
    </item>
    <item>
      <title>Why I Built a Mailbox Instead of Injecting Messages Into a Running AI Agent</title>
      <dc:creator>Adem İşler</dc:creator>
      <pubDate>Sat, 01 Aug 2026 23:38:01 +0000</pubDate>
      <link>https://dev.to/ademisler/why-i-built-a-mailbox-instead-of-injecting-messages-into-a-running-ai-agent-4l6c</link>
      <guid>https://dev.to/ademisler/why-i-built-a-mailbox-instead-of-injecting-messages-into-a-running-ai-agent-4l6c</guid>
      <description>&lt;p&gt;The most obvious way to send new guidance to a running AI coding agent is to inject another message into its active conversation.&lt;/p&gt;

&lt;p&gt;I deliberately chose not to do that.&lt;/p&gt;

&lt;p&gt;When an agent is already implementing, testing, or reasoning through a task, active steering can be useful—but it also changes the conversational state immediately. A late instruction can interrupt a coherent plan, arrive in the middle of a tool operation, or blur the line between the task the agent accepted and the new context being introduced.&lt;/p&gt;

&lt;p&gt;I wanted a different interaction model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Leave guidance now. Let the agent read it at the next safe checkpoint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That idea became &lt;strong&gt;CodexPigeon&lt;/strong&gt;, an open-source desktop app and CLI that gives each Codex worktree a small, repository-local mailbox.&lt;/p&gt;

&lt;p&gt;The human writes to an inbox. The agent reads it when control returns to a reasonable checkpoint, acknowledges the message, and can write a reply or receipt. The app observes Codex state, but it never starts, interrupts, steers, or injects into an active turn.&lt;/p&gt;

&lt;p&gt;This article explains why I chose a mailbox protocol, how the ownership boundaries work, and what I learned while designing asynchronous human-agent communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: useful context often arrives late
&lt;/h2&gt;

&lt;p&gt;Long-running software tasks rarely remain perfectly static.&lt;/p&gt;

&lt;p&gt;While an agent is working, a human may notice that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one module must not be changed;&lt;/li&gt;
&lt;li&gt;a test environment has become unavailable;&lt;/li&gt;
&lt;li&gt;a release gate should be checked before publishing;&lt;/li&gt;
&lt;li&gt;a requirement was misunderstood;&lt;/li&gt;
&lt;li&gt;another developer changed a related file;&lt;/li&gt;
&lt;li&gt;a destructive operation needs explicit approval;&lt;/li&gt;
&lt;li&gt;the final answer should include a particular validation result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The information is relevant, but the timing is awkward.&lt;/p&gt;

&lt;p&gt;Interrupting immediately is not always necessary. Waiting until the current turn ends may be too late.&lt;/p&gt;

&lt;p&gt;The product question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a human update the working context without treating the active chat stream as the only communication channel?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why I rejected active-turn injection
&lt;/h2&gt;

&lt;p&gt;Codex exposes methods that could conceptually be used to start, steer, interrupt, or inject items into conversations.&lt;/p&gt;

&lt;p&gt;CodexPigeon explicitly refuses to use them.&lt;/p&gt;

&lt;p&gt;The read-only App Server allowlist contains only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initialize
thread/list
thread/read
thread/loaded/list
hooks/list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Methods such as these are rejected by design:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;turn/steer
turn/start
turn/interrupt
thread/inject_items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is not because active steering is universally wrong. It is because CodexPigeon has a narrower contract.&lt;/p&gt;

&lt;p&gt;The product is a &lt;strong&gt;mailbox companion&lt;/strong&gt;, not a second chat client.&lt;/p&gt;

&lt;p&gt;A narrow contract makes the behavior easier to reason about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the app cannot silently alter an active conversation;&lt;/li&gt;
&lt;li&gt;the user can inspect the exact message written to disk;&lt;/li&gt;
&lt;li&gt;the agent decides when it is safe to consume the message;&lt;/li&gt;
&lt;li&gt;guidance remains attached to the repository and worktree;&lt;/li&gt;
&lt;li&gt;communication state is visible outside the chat UI;&lt;/li&gt;
&lt;li&gt;the security review can focus on a small set of file and read-only process boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The core rule: one thread, one worktree, one mailbox
&lt;/h2&gt;

&lt;p&gt;Each active worktree receives a &lt;code&gt;.codex-mailbox&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.codex-mailbox/
  INBOX.md
  OUTBOX.md
  RECEIPTS.md
  STATE.json
  HOOK_STATE.json
  README.md
  .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The operating rule is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 thread = 1 worktree = 1 .codex-mailbox/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This prevents guidance for one task from leaking into another task that happens to share the same repository.&lt;/p&gt;

&lt;p&gt;Git worktrees are a useful boundary because they already represent separate working copies, branches, and task contexts. The mailbox follows that boundary instead of inventing a global message bus with complicated routing rules.&lt;/p&gt;
&lt;h2&gt;
  
  
  File ownership is the security model
&lt;/h2&gt;

&lt;p&gt;The protocol separates files by writer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Writer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INBOX.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CodexPigeon app or CLI&lt;/td&gt;
&lt;td&gt;Human guidance for the agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OUTBOX.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex agent&lt;/td&gt;
&lt;td&gt;Human-readable replies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RECEIPTS.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex agent&lt;/td&gt;
&lt;td&gt;Acknowledgement and action status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STATE.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CodexPigeon app or CLI&lt;/td&gt;
&lt;td&gt;Technical state and optional repeats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOOK_STATE.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hook runtime&lt;/td&gt;
&lt;td&gt;Reminder throttling and runtime state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The app must never write to &lt;code&gt;OUTBOX.md&lt;/code&gt; or &lt;code&gt;RECEIPTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The agent must never write to &lt;code&gt;INBOX.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This avoids two processes editing the same logical record and makes provenance visible. When a line appears in the inbox, it came from the human-side tool. When a receipt appears, it came from the agent-side workflow.&lt;/p&gt;

&lt;p&gt;The boundary is simple enough to explain in one table, which is usually a good sign for a protocol.&lt;/p&gt;
&lt;h2&gt;
  
  
  A message is plain Markdown
&lt;/h2&gt;

&lt;p&gt;I chose Markdown rather than a hidden database protocol because the mailbox should remain inspectable with normal tools.&lt;/p&gt;

&lt;p&gt;An inbox message looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9&lt;/span&gt;

from: human
created_at: 2026-05-20T15:30:00.000Z
priority: normal
status: unread
scope: current_task

Message:
Do not touch the auth module during the billing refactor.
If auth changes are needed, ask in OUTBOX.md first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The ID combines a readable timestamp with a ULID-style suffix. That gives messages a useful chronological hint while avoiding collisions during rapid writes.&lt;/p&gt;

&lt;p&gt;Messages are append-only. The app does not later rewrite &lt;code&gt;status: unread&lt;/code&gt; inside the original inbox block.&lt;/p&gt;

&lt;p&gt;Instead, current status is derived from agent receipts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Receipts are more useful than mutating the inbox
&lt;/h2&gt;

&lt;p&gt;A receipt can say whether the agent accepted, rejected, deferred, applied, or was blocked from applying a message.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## receipt_20260520T153245_01HY4YJ4Q8E2GHK2Z6MBWR6A1R&lt;/span&gt;

message_id: msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9
seen_at: 2026-05-20T15:32:45.000Z
decision: accepted
action_status: applied
summary: Updated the current plan to avoid auth module changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The UI derives message state from these receipts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no receipt → unseen;&lt;/li&gt;
&lt;li&gt;accepted → accepted;&lt;/li&gt;
&lt;li&gt;rejected → rejected;&lt;/li&gt;
&lt;li&gt;deferred → deferred;&lt;/li&gt;
&lt;li&gt;needs confirmation → needs confirmation;&lt;/li&gt;
&lt;li&gt;applied → applied;&lt;/li&gt;
&lt;li&gt;blocked → blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This has two advantages.&lt;/p&gt;

&lt;p&gt;First, the original human message remains immutable.&lt;/p&gt;

&lt;p&gt;Second, acknowledgement and execution are separate concepts. An agent may understand an instruction but be unable to apply it, or it may defer the instruction until a later stage.&lt;/p&gt;

&lt;p&gt;A single “read” flag would lose that distinction.&lt;/p&gt;
&lt;h2&gt;
  
  
  OUTBOX is intentionally separate
&lt;/h2&gt;

&lt;p&gt;The agent can also write a human-readable reply:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## reply_20260520T153250_01HY4YJ9W9A2F33H4Y9N7DR5F0&lt;/span&gt;

from: agent
to: msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9
created_at: 2026-05-20T15:32:50.000Z

Understood. I will avoid auth and ask here first if that changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Receipts are structured status. OUTBOX is conversation-like feedback.&lt;/p&gt;

&lt;p&gt;Keeping them separate prevents the UI from having to infer operational state from free-form prose.&lt;/p&gt;
&lt;h2&gt;
  
  
  Safe checkpoints instead of immediate delivery
&lt;/h2&gt;

&lt;p&gt;The mailbox only works if the agent checks it at useful moments.&lt;/p&gt;

&lt;p&gt;CodexPigeon installs a managed section into &lt;code&gt;AGENTS.md&lt;/code&gt; that asks the agent to inspect the inbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;before a major architectural decision;&lt;/li&gt;
&lt;li&gt;after a meaningful implementation step;&lt;/li&gt;
&lt;li&gt;after tests or a long shell command;&lt;/li&gt;
&lt;li&gt;before the final response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also installs project-local hooks:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SessionStart
PostToolUse
Stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The hooks can create missing mailbox files, detect unread messages, and remind the agent to perform a final check.&lt;/p&gt;

&lt;p&gt;But hooks are not treated as a hard security boundary. They are reminder infrastructure.&lt;/p&gt;

&lt;p&gt;The primary contract remains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repository instructions tell the agent how to behave;&lt;/li&gt;
&lt;li&gt;ownership rules say which side writes each file;&lt;/li&gt;
&lt;li&gt;App Server methods remain read-only;&lt;/li&gt;
&lt;li&gt;the agent consumes mailbox content as human guidance, not executable shell input.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Preserving existing repository instructions
&lt;/h2&gt;

&lt;p&gt;Installing a tool into a real repository should not overwrite the project's existing operating rules.&lt;/p&gt;

&lt;p&gt;CodexPigeon updates only a managed block inside &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- CODEXPIGEON_MAILBOX_START --&amp;gt;&lt;/span&gt;
...
&lt;span class="c"&gt;&amp;lt;!-- CODEXPIGEON_MAILBOX_END --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Everything outside that block is preserved.&lt;/p&gt;

&lt;p&gt;The same principle applies to &lt;code&gt;.codex/hooks.json&lt;/code&gt;. Existing non-CodexPigeon hook groups remain in place, while only the managed CodexPigeon groups are replaced or updated.&lt;/p&gt;

&lt;p&gt;This was an important implementation detail. A tool that helps coordinate an agent should not destroy the instructions that define how that agent works.&lt;/p&gt;
&lt;h2&gt;
  
  
  The desktop app has two integration planes
&lt;/h2&gt;

&lt;p&gt;The architecture separates two different responsibilities.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Mailbox integration
&lt;/h3&gt;

&lt;p&gt;The app and CLI can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install the mailbox into a workspace;&lt;/li&gt;
&lt;li&gt;validate messages;&lt;/li&gt;
&lt;li&gt;append inbox entries;&lt;/li&gt;
&lt;li&gt;parse inbox, outbox, and receipt Markdown;&lt;/li&gt;
&lt;li&gt;watch files for changes;&lt;/li&gt;
&lt;li&gt;derive message status;&lt;/li&gt;
&lt;li&gt;manage optional repeated sends;&lt;/li&gt;
&lt;li&gt;inspect whether hooks and instructions are installed correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. Read-only Codex integration
&lt;/h3&gt;

&lt;p&gt;The App Server client can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;discover threads;&lt;/li&gt;
&lt;li&gt;read thread metadata;&lt;/li&gt;
&lt;li&gt;observe loaded threads;&lt;/li&gt;
&lt;li&gt;inspect hooks;&lt;/li&gt;
&lt;li&gt;enrich the UI with activity status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These planes meet in the interface, but they do not share mutation powers.&lt;/p&gt;

&lt;p&gt;The desktop app can help a user choose the correct worktree and observe the related Codex task. It still writes guidance only through the mailbox.&lt;/p&gt;
&lt;h2&gt;
  
  
  Package boundaries
&lt;/h2&gt;

&lt;p&gt;The project is a TypeScript monorepo with several focused packages.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;packages/mailbox-core&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the protocol implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Markdown parsing and serialization;&lt;/li&gt;
&lt;li&gt;message ID generation;&lt;/li&gt;
&lt;li&gt;path normalization;&lt;/li&gt;
&lt;li&gt;append locking;&lt;/li&gt;
&lt;li&gt;file watching;&lt;/li&gt;
&lt;li&gt;installer behavior;&lt;/li&gt;
&lt;li&gt;message validation;&lt;/li&gt;
&lt;li&gt;repeated-message state.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;packages/codex-app-server&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the read-only JSON-RPC client. It enforces the method allowlist at runtime rather than relying only on developer discipline.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;packages/cli&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The CLI exposes commands such as:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send
watch
install
snapshot
doctor
automation list
automation stop
automation run-due
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;packages/hooks&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This contains the managed &lt;code&gt;AGENTS.md&lt;/code&gt; block, hook configuration, Python hook runtime, mailbox README, and ignore templates.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;apps/desktop&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The Electron application owns native dialogs, filesystem watching, IPC, and the React interface.&lt;/p&gt;

&lt;p&gt;The renderer does not receive unrestricted Node access. A narrow preload bridge exposes the operations the UI needs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why parsing Markdown still required discipline
&lt;/h2&gt;

&lt;p&gt;Human-readable files do not remove the need for a real parser.&lt;/p&gt;

&lt;p&gt;The mailbox uses a Markdown syntax tree rather than splitting strings on headings or blank lines.&lt;/p&gt;

&lt;p&gt;The parser treats each H2 heading as a new message, reads initial &lt;code&gt;key: value&lt;/code&gt; metadata, and then captures the body.&lt;/p&gt;

&lt;p&gt;Malformed blocks are skipped instead of being automatically rewritten.&lt;/p&gt;

&lt;p&gt;That matters because mailbox files are both machine-readable and human-editable. A defensive parser should tolerate partial mistakes without corrupting the rest of the history.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cross-process appends and race conditions
&lt;/h2&gt;

&lt;p&gt;The app and CLI may both be active.&lt;/p&gt;

&lt;p&gt;Repeated-message scheduling may also attempt to append while a human sends a manual message.&lt;/p&gt;

&lt;p&gt;Inbox writes therefore use a cross-process lock. Missing files are created before append, and both manual and repeated messages use the same validation and write path.&lt;/p&gt;

&lt;p&gt;Separate file ownership removes the largest race condition: the human-side tools and agent never append to the same file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Optional repeated messages
&lt;/h2&gt;

&lt;p&gt;CodexPigeon can repeat guidance at an interval while the desktop app or an explicit CLI runner remains active.&lt;/p&gt;

&lt;p&gt;A repeat is stored in &lt;code&gt;STATE.json&lt;/code&gt; with fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;next run time;&lt;/li&gt;
&lt;li&gt;last send time;&lt;/li&gt;
&lt;li&gt;interval;&lt;/li&gt;
&lt;li&gt;source message ID;&lt;/li&gt;
&lt;li&gt;send count;&lt;/li&gt;
&lt;li&gt;status;&lt;/li&gt;
&lt;li&gt;whether warnings were explicitly allowed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When due, the scheduler appends a &lt;strong&gt;new normal inbox message&lt;/strong&gt;. It does not use a hidden channel and does not mutate the original message.&lt;/p&gt;

&lt;p&gt;Repeated sending is disabled by default and has a minimum interval to prevent accidental hot loops.&lt;/p&gt;

&lt;p&gt;The intended use is reminders such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Re-check the deployment gate before finalizing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is not intended for continuously pushing destructive commands into a task.&lt;/p&gt;
&lt;h2&gt;
  
  
  Message validation
&lt;/h2&gt;

&lt;p&gt;Mailbox content is prompt-like input. It can still ask the agent to do something unsafe.&lt;/p&gt;

&lt;p&gt;CodexPigeon warns on messages that appear to contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;secrets;&lt;/li&gt;
&lt;li&gt;destructive operations;&lt;/li&gt;
&lt;li&gt;risky production actions;&lt;/li&gt;
&lt;li&gt;credential-related instructions;&lt;/li&gt;
&lt;li&gt;unusually large content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Warnings block sending by default.&lt;/p&gt;

&lt;p&gt;The CLI requires an explicit override, and the UI requires a deliberate “send anyway” action.&lt;/p&gt;

&lt;p&gt;This is a user-safety layer, not a perfect secret scanner. The project still tells users not to paste credentials into mailbox messages.&lt;/p&gt;
&lt;h2&gt;
  
  
  Important limitations
&lt;/h2&gt;

&lt;p&gt;The mailbox model has honest constraints.&lt;/p&gt;
&lt;h3&gt;
  
  
  An agent cannot read while trapped inside a blocking tool call
&lt;/h3&gt;

&lt;p&gt;If a shell command runs for twenty minutes without returning control, the agent cannot inspect new mailbox messages during those twenty minutes.&lt;/p&gt;

&lt;p&gt;That is a consequence of safe-checkpoint delivery rather than active interruption.&lt;/p&gt;
&lt;h3&gt;
  
  
  Markdown is not a transactional database
&lt;/h3&gt;

&lt;p&gt;The files should remain reasonably small and append-only. The design favors transparency and local inspectability over high-throughput messaging.&lt;/p&gt;
&lt;h3&gt;
  
  
  Worktree handoffs can lose ignored runtime files
&lt;/h3&gt;

&lt;p&gt;Mailbox runtime files are intentionally ignored by Git because they may contain private working context. Some worktree or repository handoff patterns therefore will not carry them automatically.&lt;/p&gt;

&lt;p&gt;The app and hooks recreate missing files when needed, but the runtime history is local by design.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hooks require a trusted project
&lt;/h3&gt;

&lt;p&gt;If Codex does not trust the repository, project-local hook layers may be ignored. The UI and doctor command need to make that situation visible.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Not every human-agent interaction belongs in chat
&lt;/h3&gt;

&lt;p&gt;A repository task has state outside the conversation: files, plans, test results, worktrees, and constraints. A repo-local mailbox can be a better place for durable working guidance.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Refusing capabilities can strengthen a product
&lt;/h3&gt;

&lt;p&gt;CodexPigeon would be more powerful if it could directly steer or interrupt turns. It would also be a different and harder-to-reason-about product.&lt;/p&gt;

&lt;p&gt;The explicit refusal to use mutation APIs created a clearer identity.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Ownership tables are powerful design tools
&lt;/h3&gt;

&lt;p&gt;The rule “the app writes inbox, the agent writes outbox and receipts” removed entire classes of ambiguity and write races.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Acknowledgement is not the same as execution
&lt;/h3&gt;

&lt;p&gt;Structured receipts made it possible to distinguish seen, accepted, deferred, applied, blocked, and rejected states.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Transparent protocols are easier to debug
&lt;/h3&gt;

&lt;p&gt;When something goes wrong, the user can open a Markdown file and inspect what was sent, what the agent acknowledged, and which side owns the next action.&lt;/p&gt;
&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;CodexPigeon is a working MVP for local macOS and Linux development.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Electron and React desktop application;&lt;/li&gt;
&lt;li&gt;a CLI;&lt;/li&gt;
&lt;li&gt;a mailbox parser and installer;&lt;/li&gt;
&lt;li&gt;read-only Codex thread discovery;&lt;/li&gt;
&lt;li&gt;project-local hooks;&lt;/li&gt;
&lt;li&gt;diagnostics and doctor commands;&lt;/li&gt;
&lt;li&gt;automated tests for protocol, installer, hooks, and App Server boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signed desktop packages and a Windows packaging path remain future work.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ademisler" rel="noopener noreferrer"&gt;
        ademisler
      &lt;/a&gt; / &lt;a href="https://github.com/ademisler/codexpigeon" rel="noopener noreferrer"&gt;
        codexpigeon
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Non-interrupting mailbox companion for Codex worktrees on macOS and Linux, with repo-local inbox files, read-only App Server discovery, and safe checkpoint reminders.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/ademisler/codexpigeon/docs/assets/hero/codexpigeon-hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fademisler%2Fcodexpigeon%2FHEAD%2Fdocs%2Fassets%2Fhero%2Fcodexpigeon-hero.png" alt="CodexPigeon hero" width="100%"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CodexPigeon&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  Non-interrupting mailbox companion for Codex worktrees
&lt;/p&gt;

&lt;p&gt;
  Built by &lt;a href="https://github.com/ademisler" rel="noopener noreferrer"&gt;Adem Isler&lt;/a&gt; · MIT licensed · macOS and Linux development builds
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/ademisler/codexpigeon/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img alt="CI" src="https://github.com/ademisler/codexpigeon/actions/workflows/ci.yml/badge.svg"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/ademisler/codexpigeon/LICENSE" rel="noopener noreferrer"&gt;&lt;img alt="License: MIT" src="https://camo.githubusercontent.com/b3b84cbd7dba6b5a4cb102224ecd9d3c89505eadd576a36bf9c7dc97d6f6cdff/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d6639373331362e737667"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/9fbec56b5a3e960b03ac133e93f75380034b38f4c53dc69f28084ad6c8b52879/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f532532302532422532304c696e75782d3462353536332e737667"&gt;&lt;img alt="Platform: macOS and Linux" src="https://camo.githubusercontent.com/9fbec56b5a3e960b03ac133e93f75380034b38f4c53dc69f28084ad6c8b52879/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f532532302532422532304c696e75782d3462353536332e737667"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What This Is&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;CodexPigeon lets a human leave repo-local guidance for a Codex task without
injecting into the active chat turn.&lt;/p&gt;
&lt;p&gt;It gives each selected repo/worktree a &lt;code&gt;.codex-mailbox/&lt;/code&gt; folder. The app and CLI
append human messages to &lt;code&gt;INBOX.md&lt;/code&gt;; the Codex agent reads those messages at
safe checkpoints through installed project instructions and hooks, then writes
status back to agent-owned files.&lt;/p&gt;
&lt;p&gt;The product invariant is strict:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no active chat steering&lt;/li&gt;
&lt;li&gt;no chat message injection&lt;/li&gt;
&lt;li&gt;no turn interruption or turn start APIs&lt;/li&gt;
&lt;li&gt;no App Server write APIs&lt;/li&gt;
&lt;li&gt;no app writes to agent-owned &lt;code&gt;OUTBOX.md&lt;/code&gt; or &lt;code&gt;RECEIPTS.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Open Source&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;This repository is intended to be inspectable. The mailbox protocol is plain
Markdown, runtime files are repo-local, and all Codex App Server access is
restricted to read/status/discovery methods.&lt;/p&gt;
&lt;p&gt;The current release is a working…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ademisler/codexpigeon" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I would particularly value feedback on these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which safe checkpoints make sense during a long coding task?&lt;/li&gt;
&lt;li&gt;Is plain Markdown the right tradeoff for human-agent working state?&lt;/li&gt;
&lt;li&gt;Which receipt states would be useful beyond accepted, deferred, applied, and blocked?&lt;/li&gt;
&lt;li&gt;Where should the boundary sit between asynchronous guidance and active steering?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the third article in my series documenting the products I have built, moving from browser utilities toward AI-native developer tools and local desktop systems.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How I Ran Custom Model Providers in Separate Codex Desktop Apps</title>
      <dc:creator>Adem İşler</dc:creator>
      <pubDate>Sat, 01 Aug 2026 23:36:19 +0000</pubDate>
      <link>https://dev.to/ademisler/how-i-ran-custom-model-providers-in-separate-codex-desktop-apps-1gjm</link>
      <guid>https://dev.to/ademisler/how-i-ran-custom-model-providers-in-separate-codex-desktop-apps-1gjm</guid>
      <description>&lt;p&gt;After building Toolboard, I started exploring a different kind of product problem: how to use a custom model provider inside Codex Desktop without turning the main installation into an experimental environment.&lt;/p&gt;

&lt;p&gt;The obvious approach was to edit the existing configuration and point it at another endpoint.&lt;/p&gt;

&lt;p&gt;I did not want to do that.&lt;/p&gt;

&lt;p&gt;My normal Codex setup already had its own sessions, configuration, automations, icon, launcher, and authentication state. A custom provider should not overwrite any of it. It should behave like a separate application that can live beside the original.&lt;/p&gt;

&lt;p&gt;That requirement became &lt;strong&gt;Codex Desktop Custom Models&lt;/strong&gt;: an open-source starter kit for creating isolated Codex Desktop apps on macOS and Linux.&lt;/p&gt;

&lt;p&gt;The reference implementation is &lt;strong&gt;Codex MiniMax&lt;/strong&gt;, a separately branded Codex Desktop app that uses MiniMax through a local compatibility bridge.&lt;/p&gt;

&lt;p&gt;This article explains the architecture behind it, the API mismatch that required a bridge, and why isolation turned out to be the most important product decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem was not changing the model
&lt;/h2&gt;

&lt;p&gt;Changing a model name is easy.&lt;/p&gt;

&lt;p&gt;Creating a second desktop application that does not interfere with the first one is a larger systems problem.&lt;/p&gt;

&lt;p&gt;A custom Codex app needs its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application name and icon;&lt;/li&gt;
&lt;li&gt;bundle or desktop application identity;&lt;/li&gt;
&lt;li&gt;launcher and executable path;&lt;/li&gt;
&lt;li&gt;webview port;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CODEX_HOME&lt;/code&gt; directory;&lt;/li&gt;
&lt;li&gt;provider configuration;&lt;/li&gt;
&lt;li&gt;API credentials;&lt;/li&gt;
&lt;li&gt;sessions and local state;&lt;/li&gt;
&lt;li&gt;bridge process;&lt;/li&gt;
&lt;li&gt;automation definitions and runner state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If even one of those boundaries is shared accidentally, the two apps can collide in subtle ways.&lt;/p&gt;

&lt;p&gt;They may open with the wrong icon, share session history, overwrite configuration, reuse the same webview port, or make it unclear which provider is active.&lt;/p&gt;

&lt;p&gt;So I made isolation the primary invariant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A custom Codex app should feel familiar, but it should not share identity, state, ports, or provider configuration with the primary installation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the isolated setup looks like
&lt;/h2&gt;

&lt;p&gt;A normal installation and a custom profile can exist side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Original Codex&lt;/th&gt;
&lt;th&gt;Custom Codex app&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default application identity&lt;/td&gt;
&lt;td&gt;Unique app name, icon, and app ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main &lt;code&gt;~/.codex&lt;/code&gt; directory&lt;/td&gt;
&lt;td&gt;Separate directory such as &lt;code&gt;~/.codex-minimax&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default provider configuration&lt;/td&gt;
&lt;td&gt;Custom provider configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing sessions and state&lt;/td&gt;
&lt;td&gt;Independent sessions and state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing automations&lt;/td&gt;
&lt;td&gt;Separate local automation layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the MiniMax reference profile, the resulting flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Codex MiniMax Desktop
        |
        v
custom launcher and application identity
        |
        v
CODEX_HOME=~/.codex-minimax
        |
        v
local bridge at 127.0.0.1:4007/v1/responses
        |
        v
MiniMax OpenAI-compatible /chat/completions endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The original Codex installation is left untouched.&lt;/p&gt;

&lt;p&gt;On macOS, the installer creates a separate app bundle under &lt;code&gt;~/Applications&lt;/code&gt;, command wrappers under &lt;code&gt;~/.local/bin&lt;/code&gt;, an isolated home directory, and a LaunchAgent for the bridge process.&lt;/p&gt;

&lt;p&gt;On Linux, it creates a side-by-side desktop shell under the user's local application directories and reuses large runtime assets where practical.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why a local bridge was necessary
&lt;/h2&gt;

&lt;p&gt;The model provider was not the only compatibility layer.&lt;/p&gt;

&lt;p&gt;Codex Desktop communicates with a provider using the &lt;strong&gt;Responses API shape&lt;/strong&gt;. Many third-party providers expose an OpenAI-compatible &lt;strong&gt;Chat Completions API&lt;/strong&gt; instead.&lt;/p&gt;

&lt;p&gt;Those APIs overlap, but they are not interchangeable.&lt;/p&gt;

&lt;p&gt;The bridge has to translate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responses input items into chat messages;&lt;/li&gt;
&lt;li&gt;developer instructions into the correct upstream role;&lt;/li&gt;
&lt;li&gt;assistant messages and output text;&lt;/li&gt;
&lt;li&gt;function calls and function-call outputs;&lt;/li&gt;
&lt;li&gt;namespaced tools;&lt;/li&gt;
&lt;li&gt;tool-choice behavior;&lt;/li&gt;
&lt;li&gt;streaming and non-streaming output;&lt;/li&gt;
&lt;li&gt;provider-specific model names;&lt;/li&gt;
&lt;li&gt;errors and health checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The basic path is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/responses
        |
        v
normalize Codex input and tools
        |
        v
POST &amp;lt;provider&amp;gt;/chat/completions
        |
        v
convert assistant text and tool calls
        |
        v
Responses-compatible output for Codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Text-only requests are the easy case.&lt;/p&gt;

&lt;p&gt;Tool calls are where the design becomes more interesting.&lt;/p&gt;
&lt;h2&gt;
  
  
  Translating namespaced tools
&lt;/h2&gt;

&lt;p&gt;Codex can represent tools inside namespaces. A Chat Completions provider usually expects a flat function name.&lt;/p&gt;

&lt;p&gt;For example, a conceptual Codex tool might look like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace: github
function: fetch_file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The bridge flattens that into a provider-safe function name such as:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github__fetch_file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It keeps an internal map so that when the provider returns a tool call, the original namespace and function name can be restored before the result goes back to Codex.&lt;/p&gt;

&lt;p&gt;This sounds like a small transformation, but it is essential. Without a reversible mapping, the model may call a function successfully while Codex cannot route the result to the correct tool.&lt;/p&gt;

&lt;p&gt;The bridge also sanitizes names because provider function-name rules can be stricter than the identifiers used by the source tool system.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I used a local HTTP service
&lt;/h2&gt;

&lt;p&gt;The bridge runs only on &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That decision provided a few useful properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Codex can treat it like a normal provider endpoint;&lt;/li&gt;
&lt;li&gt;the upstream API key stays in the custom profile environment;&lt;/li&gt;
&lt;li&gt;provider-specific translation logic remains outside the desktop application bundle;&lt;/li&gt;
&lt;li&gt;health checks and test requests are easy to run;&lt;/li&gt;
&lt;li&gt;the bridge can be restarted independently;&lt;/li&gt;
&lt;li&gt;another OpenAI-compatible provider can reuse the same architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The custom configuration points Codex to the local Responses endpoint, while the bridge points to the actual provider's Chat Completions endpoint.&lt;/p&gt;

&lt;p&gt;The reference profile uses MiniMax, but the architecture is deliberately generic. A different provider mostly requires changing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the upstream base URL;&lt;/li&gt;
&lt;li&gt;the default model name;&lt;/li&gt;
&lt;li&gt;the supported model list;&lt;/li&gt;
&lt;li&gt;the API-key environment variable;&lt;/li&gt;
&lt;li&gt;any provider-specific response normalization.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Application isolation on macOS
&lt;/h2&gt;

&lt;p&gt;macOS application identity is more than a folder name.&lt;/p&gt;

&lt;p&gt;A side-by-side application needs a unique bundle identifier, display name, icon, helper identity, launcher behavior, and webview port. Copying the base &lt;code&gt;.app&lt;/code&gt; bundle gives the custom profile a controlled place to patch those values.&lt;/p&gt;

&lt;p&gt;The installer then creates wrappers such as:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;codex-minimax
codex-minimax-desktop
codex-minimax-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each wrapper has one narrow responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoke the Codex CLI with the isolated home;&lt;/li&gt;
&lt;li&gt;launch the custom desktop bundle;&lt;/li&gt;
&lt;li&gt;manage, test, and inspect the bridge process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bridge is registered through a LaunchAgent so it can survive beyond the shell session that created it.&lt;/p&gt;

&lt;p&gt;This matters because a desktop app should not depend on the user remembering to keep a terminal command running.&lt;/p&gt;
&lt;h2&gt;
  
  
  Application isolation on Linux
&lt;/h2&gt;

&lt;p&gt;The Linux path has a different set of constraints.&lt;/p&gt;

&lt;p&gt;Many large runtime files can be reused, but identity-sensitive files should be copied rather than linked.&lt;/p&gt;

&lt;p&gt;The custom app keeps independent versions of things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the launcher script;&lt;/li&gt;
&lt;li&gt;executable identity;&lt;/li&gt;
&lt;li&gt;branding assets;&lt;/li&gt;
&lt;li&gt;desktop entry metadata;&lt;/li&gt;
&lt;li&gt;the application directory structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large internal runtime assets can be linked where doing so does not create shared writable state.&lt;/p&gt;

&lt;p&gt;The rule is not “copy everything” or “symlink everything.” The rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Copy the files that define application identity, and reuse only the files that are effectively immutable runtime dependencies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That avoids unnecessary duplication without creating Dock, taskbar, icon, or profile collisions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Separate &lt;code&gt;CODEX_HOME&lt;/code&gt; is the real boundary
&lt;/h2&gt;

&lt;p&gt;The most important directory is not the app bundle. It is the custom &lt;code&gt;CODEX_HOME&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A profile such as &lt;code&gt;~/.codex-minimax&lt;/code&gt; contains the custom app's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;config.toml&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;provider environment files;&lt;/li&gt;
&lt;li&gt;bridge scripts;&lt;/li&gt;
&lt;li&gt;automation definitions;&lt;/li&gt;
&lt;li&gt;session and application state;&lt;/li&gt;
&lt;li&gt;local logs and SQLite files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The launcher exports this directory before invoking Codex.&lt;/p&gt;

&lt;p&gt;That means the custom app does not need to mutate &lt;code&gt;~/.codex&lt;/code&gt;, and removing the custom application does not require deleting the user's main Codex state.&lt;/p&gt;

&lt;p&gt;The uninstall process intentionally keeps the custom home by default. Removing the app and deleting its history are separate decisions.&lt;/p&gt;

&lt;p&gt;That distinction is valuable in any desktop tool: uninstalling executable files should not silently destroy user data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automations required a second compatibility layer
&lt;/h2&gt;

&lt;p&gt;A custom Codex profile does not automatically inherit automation tools supplied by the host environment.&lt;/p&gt;

&lt;p&gt;So the project includes a small local MCP server and runner.&lt;/p&gt;

&lt;p&gt;The MCP server exposes an automation update tool and stores definitions under the isolated custom home. It also mirrors enough metadata into local SQLite state for the app to display those automations.&lt;/p&gt;

&lt;p&gt;The runner polls for due jobs and supports two patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;background or inbox-style tasks;&lt;/li&gt;
&lt;li&gt;same-thread follow-ups.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same-thread delivery was the important detail.&lt;/p&gt;

&lt;p&gt;Creating a scheduled record is not the same as making the result appear in the conversation where the user requested it.&lt;/p&gt;

&lt;p&gt;For same-thread reminders, the automation stores the target thread ID and later runs a command conceptually equivalent to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex &lt;span class="nb"&gt;exec &lt;/span&gt;resume &amp;lt;thread-id&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That allows the follow-up to return to the original thread instead of creating disconnected output elsewhere.&lt;/p&gt;

&lt;p&gt;The scheduler is intentionally limited to common minutely, hourly, and daily patterns. It is not trying to replace a full calendar system.&lt;/p&gt;
&lt;h2&gt;
  
  
  Safety and repository hygiene
&lt;/h2&gt;

&lt;p&gt;A project that works with custom model providers and local Codex state has an obvious publishing risk: accidentally committing credentials or private session data.&lt;/p&gt;

&lt;p&gt;The repository includes a verification command that checks syntax and scans for patterns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API keys;&lt;/li&gt;
&lt;li&gt;personal home-directory paths;&lt;/li&gt;
&lt;li&gt;local project names;&lt;/li&gt;
&lt;li&gt;private state files;&lt;/li&gt;
&lt;li&gt;provider environment files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public starter kit contains templates and examples, not a copy of a real Codex profile.&lt;/p&gt;

&lt;p&gt;The files that must remain private include provider keys, session indexes, shell snapshots, logs, and application state databases.&lt;/p&gt;

&lt;p&gt;This is also why I prefer isolated profiles to modifying the primary installation. Isolation makes the security boundary easier to understand and easier to audit.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Compatibility is broader than API syntax
&lt;/h3&gt;

&lt;p&gt;The request and response formats were only one layer. A real desktop integration also needed process management, state isolation, app identity, ports, icons, launchers, and automation behavior.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Side-by-side products need explicit identity
&lt;/h3&gt;

&lt;p&gt;A renamed folder is not a separate application. Operating systems use bundle IDs, executable paths, helper identities, and desktop metadata to decide how apps are grouped and launched.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Tool calling makes provider bridges significantly harder
&lt;/h3&gt;

&lt;p&gt;Text can often be mapped directly. Namespaces, function-call IDs, tool outputs, and provider naming restrictions require reversible and carefully validated transformations.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Local bridges are useful architectural seams
&lt;/h3&gt;

&lt;p&gt;Keeping provider adaptation in a small localhost service allowed the desktop shell, Codex configuration, and upstream provider logic to remain independently understandable.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Isolation is a product feature
&lt;/h3&gt;

&lt;p&gt;The strongest benefit is not that the project can connect to MiniMax. It is that a user can experiment without destabilizing the setup they already rely on.&lt;/p&gt;
&lt;h2&gt;
  
  
  Who this project is for
&lt;/h2&gt;

&lt;p&gt;Codex Desktop Custom Models is not a one-click consumer application.&lt;/p&gt;

&lt;p&gt;It is an operator-friendly starter kit for developers who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;already have Codex Desktop working;&lt;/li&gt;
&lt;li&gt;understand that upstream internals may change;&lt;/li&gt;
&lt;li&gt;want a separate app for an OpenAI-compatible provider;&lt;/li&gt;
&lt;li&gt;prefer inspectable scripts over modifying their primary environment;&lt;/li&gt;
&lt;li&gt;are comfortable testing integrations after Codex updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is unofficial and is not an OpenAI, MiniMax, or Ollama project.&lt;/p&gt;
&lt;h2&gt;
  
  
  The repository
&lt;/h2&gt;

&lt;p&gt;The repository includes the provider bridge, macOS and Linux installers, configuration templates, automation MCP and runner, diagrams, screenshots, troubleshooting notes, and a publishing checklist.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ademisler" rel="noopener noreferrer"&gt;
        ademisler
      &lt;/a&gt; / &lt;a href="https://github.com/ademisler/codex-desktop-custom-models" rel="noopener noreferrer"&gt;
        codex-desktop-custom-models
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Open-source starter kit for isolated Codex Desktop apps for custom model providers on macOS and Linux, with provider bridges, custom icons, and local automations.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/ademisler/codex-desktop-custom-models/docs/assets/hero/codex-desktop-custom-models-hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fademisler%2Fcodex-desktop-custom-models%2FHEAD%2Fdocs%2Fassets%2Fhero%2Fcodex-desktop-custom-models-hero.png" alt="Codex Desktop Custom Models" width="100%"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Codex Desktop Custom Models&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  Create isolated Codex Desktop apps for custom model providers on macOS and
  Linux, with separate icons, state, provider bridges, and automations
&lt;/p&gt;

&lt;p&gt;
  An open-source project by &lt;a href="https://github.com/ademisler" rel="noopener noreferrer"&gt;Adem İşler&lt;/a&gt;
  Released under the MIT License.
&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What This Is&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Codex Desktop Custom Models is a starter kit for people who already have Codex
Desktop installed and want a second, independent Codex app for a custom model
provider.&lt;/p&gt;
&lt;p&gt;The reference app is &lt;strong&gt;Codex MiniMax&lt;/strong&gt;: a red-icon Codex Desktop profile that
uses MiniMax M2.7 through a local OpenAI-compatible bridge.&lt;/p&gt;
&lt;p&gt;It does not modify your original Codex app. Your normal Codex install keeps its
own config, data, sessions, provider, icon, and launcher.&lt;/p&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/ademisler/codex-desktop-custom-models/docs/assets/screenshots/codex-minimax-app.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fademisler%2Fcodex-desktop-custom-models%2FHEAD%2Fdocs%2Fassets%2Fscreenshots%2Fcodex-minimax-app.png" alt="Codex MiniMax running as a separate Codex Desktop app" width="100%"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Open Source&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;This project is released under the MIT License. You can use it, fork it, adapt
it for other providers, and contribute fixes through pull requests.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What You Get&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Isolated app clone&lt;/td&gt;
&lt;td&gt;Creates a side-by-side Codex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ademisler/codex-desktop-custom-models" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I am interested in feedback on a few areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which other OpenAI-compatible providers would be useful as documented examples?&lt;/li&gt;
&lt;li&gt;Should provider adapters remain configuration-driven, or should complex providers get separate bridge modules?&lt;/li&gt;
&lt;li&gt;Which parts of desktop-app isolation have caused the most trouble in your own tooling?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the second article in my series documenting the products I have built, moving from browser utilities toward AI-native developer tools and desktop systems.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>tutorial</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How I Turned a Cluttered Browser Workflow Into a Chrome Extension With 83 Tools</title>
      <dc:creator>Adem İşler</dc:creator>
      <pubDate>Sat, 01 Aug 2026 23:28:39 +0000</pubDate>
      <link>https://dev.to/ademisler/how-i-turned-a-cluttered-browser-workflow-into-a-chrome-extension-with-83-tools-28c2</link>
      <guid>https://dev.to/ademisler/how-i-turned-a-cluttered-browser-workflow-into-a-chrome-extension-with-83-tools-28c2</guid>
      <description>&lt;p&gt;As I started documenting the products I have built over time, I decided to begin with one of the earliest: &lt;strong&gt;Toolboard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Browser workflows rarely become frustrating because one important feature is missing. They become frustrating because useful actions are scattered across separate extensions, websites, menus, and tabs.&lt;/p&gt;

&lt;p&gt;I had one extension for screenshots, another for colors, another for fonts, separate websites for data conversion, and a growing set of small utilities I repeatedly searched for. Each tool solved a real problem. Together, they created a fragmented workflow.&lt;/p&gt;

&lt;p&gt;So I built one place for them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ademisler/toolboard" rel="noopener noreferrer"&gt;Toolboard&lt;/a&gt; is a Manifest V3 Chrome extension that brings &lt;strong&gt;83 browser tools&lt;/strong&gt; into a single searchable interface. It covers inspection, capture, browsing enhancements, general utilities, format conversion, structured-data previewing, and AI-assisted workflows.&lt;/p&gt;

&lt;p&gt;This post is less about listing 83 features and more about the product and engineering decisions required to stop 83 features from becoming a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product boundary
&lt;/h2&gt;

&lt;p&gt;The initial idea was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put the small browser tools I use regularly behind one consistent interface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That boundary mattered. Toolboard was not meant to become a second browser or a giant automation platform. It was meant to reduce the friction between noticing a task and completing it.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspecting a color, font, element, or link on the current page;&lt;/li&gt;
&lt;li&gt;capturing a screenshot, page text, media, PDF, QR code, or tab recording;&lt;/li&gt;
&lt;li&gt;adding sticky notes, highlights, reading mode, bookmarks, or dark mode;&lt;/li&gt;
&lt;li&gt;converting units, currencies, time zones, colors, timestamps, encodings, data formats, images, and subtitles;&lt;/li&gt;
&lt;li&gt;previewing JSON, CSV/TSV, Markdown, XML, PDFs, images, OpenGraph metadata, schema data, and links;&lt;/li&gt;
&lt;li&gt;summarizing, translating, analyzing, or discussing page content with AI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the project grew, the tools settled into seven categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inspect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Color Picker, Element Picker, Font Finder, Link Picker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Capture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Screenshot Picker, Media Download, Text Picker, PDF Generator, QR Generator, Video Recorder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enhance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sticky Notes, Reading Mode, Text Highlighter, Bookmark Manager, Dark Mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Utilities&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Site Info, Color Palette Generator, Copy History, Macro Recorder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Converters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Units, currency, time zones, Base64, URLs, UUIDs, hashes, JWTs, JSON/YAML, CSV/JSON, XML/JSON, images, media, subtitles, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Previewers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON, CSV/TSV, Markdown, XML, PDF, image, OpenGraph, schema, and link previews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Summarizer, Translator, Content Detector, Email Generator, SEO Analyzer, AI Chat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The breadth is useful, but breadth immediately creates a second problem: &lt;strong&gt;discoverability&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding tools was not the hard part
&lt;/h2&gt;

&lt;p&gt;The difficult part was not writing another converter or picker. The difficult part was keeping dozens of tools understandable, searchable, testable, and consistent.&lt;/p&gt;

&lt;p&gt;Toolboard uses a central tool manifest as its product registry. Every tool declares information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color-picker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Color Picker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inspect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inspect/colorPicker.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"design"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"palette"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hex"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rgb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyedropper"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"activeTab"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This manifest became the source of truth for the interface and release process.&lt;/p&gt;

&lt;p&gt;It lets the extension:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build categories without hard-coding every card;&lt;/li&gt;
&lt;li&gt;search by names, tags, and related keywords;&lt;/li&gt;
&lt;li&gt;keep ordering predictable;&lt;/li&gt;
&lt;li&gt;connect tools to icons and localization keys;&lt;/li&gt;
&lt;li&gt;associate functionality with the permissions it needs;&lt;/li&gt;
&lt;li&gt;detect missing modules or inconsistent registrations before release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That structure was more important than any individual feature. Without it, every new tool would have increased the amount of manual UI code and made future changes more fragile.&lt;/p&gt;
&lt;h2&gt;
  
  
  Designing for 83 choices
&lt;/h2&gt;

&lt;p&gt;A grid containing 83 equally weighted options is not a productivity interface. It is a directory.&lt;/p&gt;

&lt;p&gt;I added several layers to reduce that cognitive load:&lt;/p&gt;
&lt;h3&gt;
  
  
  Search
&lt;/h3&gt;

&lt;p&gt;Users can search by tool name as well as related terms. Someone looking for “hex,” “eyedropper,” or “palette” should still reach the Color Picker.&lt;/p&gt;
&lt;h3&gt;
  
  
  Categories
&lt;/h3&gt;

&lt;p&gt;The seven categories create a first-level mental model. A user does not need to remember the exact name of a tool if they know whether they want to inspect, capture, convert, or preview something.&lt;/p&gt;
&lt;h3&gt;
  
  
  Favorites and usage-based ordering
&lt;/h3&gt;

&lt;p&gt;Frequently used tools can be marked as favorites. Favorites rise to the top, and usage frequency helps order tools within that group.&lt;/p&gt;

&lt;p&gt;The goal is for Toolboard to become smaller for each user over time. The extension may contain 83 tools, but a person should mostly see the subset relevant to their own workflow.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hidden tools
&lt;/h3&gt;

&lt;p&gt;Not every feature is useful to every person. Tools can be hidden instead of forcing everyone to navigate the full catalog forever.&lt;/p&gt;
&lt;h3&gt;
  
  
  Consistent interaction patterns
&lt;/h3&gt;

&lt;p&gt;Dozens of tools should not feel like dozens of unrelated mini-apps. Buttons, panels, previews, loading states, copy actions, downloads, and completion feedback need to behave consistently.&lt;/p&gt;

&lt;p&gt;I even added small multilingual coffee-themed messages after successful operations. It is a minor detail, but consistency in small feedback moments helps a broad utility suite feel like one product rather than a folder of scripts.&lt;/p&gt;
&lt;h2&gt;
  
  
  The permission model is part of the product
&lt;/h2&gt;

&lt;p&gt;A multi-tool browser extension naturally needs a wider permission surface than a single-purpose extension.&lt;/p&gt;

&lt;p&gt;Toolboard uses permissions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;activeTab&lt;/code&gt; and &lt;code&gt;scripting&lt;/code&gt; for page inspection and tool activation;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;storage&lt;/code&gt; for preferences, favorites, notes, bookmarks, highlights, and local history;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clipboardRead&lt;/code&gt; and &lt;code&gt;clipboardWrite&lt;/code&gt; for copy-related workflows;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;downloads&lt;/code&gt; for generated or captured files;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tabCapture&lt;/code&gt; for the video recorder;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tabs&lt;/code&gt; where tab metadata is needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension also needs to operate across normal HTTP and HTTPS pages so the tools are available where the user needs them.&lt;/p&gt;

&lt;p&gt;This makes transparency essential. A permission list should not be treated as a technical footnote. Users should be able to understand which capability needs each permission and what happens to their data.&lt;/p&gt;

&lt;p&gt;Most Toolboard state stays in &lt;code&gt;chrome.storage.local&lt;/code&gt;, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;favorites and hidden tools;&lt;/li&gt;
&lt;li&gt;per-site sticky notes and highlights;&lt;/li&gt;
&lt;li&gt;bookmarks and copy history;&lt;/li&gt;
&lt;li&gt;theme and onboarding preferences;&lt;/li&gt;
&lt;li&gt;tool usage data used for sorting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no analytics or crash telemetry built into the extension.&lt;/p&gt;

&lt;p&gt;AI features are different because they need a model endpoint. They use the user's own Gemini API key, and the relevant selected text or page content is sent to Gemini only when the user invokes an AI tool. API keys are encrypted locally before storage. A small number of converter features also use task-specific external services, such as exchange-rate or label-rendering APIs.&lt;/p&gt;

&lt;p&gt;The lesson for me was simple: &lt;strong&gt;privacy is not a badge added to a README. It is a product surface that needs explicit controls, explanations, and boundaries.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  AI as a tool category, not the entire product
&lt;/h2&gt;

&lt;p&gt;Toolboard includes an AI Summarizer, Translator, Content Detector, Email Generator, SEO Analyzer, and a page-aware chat interface.&lt;/p&gt;

&lt;p&gt;But I did not want every utility to become a chat prompt.&lt;/p&gt;

&lt;p&gt;A color picker should still pick colors. A JWT decoder should still decode a token. A CSV previewer should still show a table immediately.&lt;/p&gt;

&lt;p&gt;AI is useful where interpretation is required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;condensing a long page;&lt;/li&gt;
&lt;li&gt;translating selected or full-page text;&lt;/li&gt;
&lt;li&gt;generating a structured email;&lt;/li&gt;
&lt;li&gt;reviewing SEO signals;&lt;/li&gt;
&lt;li&gt;discussing the current page with context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is less useful when a deterministic local operation can provide an immediate and reliable result.&lt;/p&gt;

&lt;p&gt;Keeping AI as one category among seven helped preserve that distinction.&lt;/p&gt;
&lt;h2&gt;
  
  
  Release gates for a large extension
&lt;/h2&gt;

&lt;p&gt;Feature count creates maintenance risk. A small change to the registry, icons, translations, or activation path can break tools that appear unrelated.&lt;/p&gt;

&lt;p&gt;The release process therefore checks more than whether the JavaScript parses.&lt;/p&gt;

&lt;p&gt;Automated tests validate areas such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tool IDs being unique and consistently formatted;&lt;/li&gt;
&lt;li&gt;registered modules existing and exporting the expected shape;&lt;/li&gt;
&lt;li&gt;localization keys being present across English, Turkish, and French;&lt;/li&gt;
&lt;li&gt;every tool having a registered icon;&lt;/li&gt;
&lt;li&gt;the manifest and displayed tool count staying aligned;&lt;/li&gt;
&lt;li&gt;shared completion-message coverage remaining complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a manual release checklist for preview rendering, repeated tool execution, fullscreen behavior, overlay closing, light/dark contrast, localization, and permission consistency.&lt;/p&gt;

&lt;p&gt;This is one of the biggest lessons I took from Toolboard:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shipping many small features does not reduce the need for engineering discipline. It multiplies it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Feature breadth changes the core problem
&lt;/h3&gt;

&lt;p&gt;At first, the problem was building useful tools. Later, the problem became helping people find and trust the right tool quickly.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. A registry-driven architecture scales better than manual UI wiring
&lt;/h3&gt;

&lt;p&gt;The manifest gave the project a stable vocabulary for tools, categories, keywords, icons, localization, and permissions.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Consistency matters more as individual features become smaller
&lt;/h3&gt;

&lt;p&gt;When each action is brief, inconsistent buttons, feedback, previews, or output handling become disproportionately noticeable.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Browser permissions require product communication
&lt;/h3&gt;

&lt;p&gt;Technical justification is not enough. Permission use has to be understandable from the user's point of view.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. AI should be applied selectively
&lt;/h3&gt;

&lt;p&gt;AI added value to interpretation-heavy workflows, but many browser tasks remained better as fast, local, deterministic utilities.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;Toolboard is currently at version 3.0.1 and remains open source under the MIT License.&lt;/p&gt;

&lt;p&gt;The repository includes the extension source, privacy documentation, tests, and release checklist:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ademisler" rel="noopener noreferrer"&gt;
        ademisler
      &lt;/a&gt; / &lt;a href="https://github.com/ademisler/toolboard" rel="noopener noreferrer"&gt;
        toolboard
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A Chrome extension with 80+ productivity tools for inspection, capture, conversion, and AI-assisted workflows.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Toolboard&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A comprehensive Chrome extension with 83 web productivity tools, including AI-powered features, a smart favorite system, converter suite, and coffee toast messages.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Features&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;AI-Powered Tools&lt;/h3&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Summarizer&lt;/strong&gt; - Intelligent text summarization with multiple length options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Translator&lt;/strong&gt; - Real-time translation with in-place page translation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Content Detector&lt;/strong&gt; - Detect AI-generated content with detailed analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Email Generator&lt;/strong&gt; - Professional email generation with customizable tone and type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI SEO Analyzer&lt;/strong&gt; - Comprehensive SEO analysis with AI-powered scoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Chat&lt;/strong&gt; - Intelligent conversational interface with persistent page context awareness&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Inspection Tools&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Color Picker&lt;/strong&gt; - Extract colors from any webpage element&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element Picker&lt;/strong&gt; - Inspect DOM elements and their properties&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font Picker&lt;/strong&gt; - Analyze fonts used on web pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link Picker&lt;/strong&gt; - Validate and analyze links&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Capture Tools&lt;/h3&gt;

&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;strong&gt;Screenshot Picker&lt;/strong&gt; - Capture full page or selected area screenshots&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Text Picker&lt;/strong&gt; - Extract text from web pages&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Media Picker&lt;/strong&gt; - Extract images…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ademisler/toolboard" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;I am especially interested in feedback on three areas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the category structure match how you think about browser tools?&lt;/li&gt;
&lt;li&gt;Which permissions or privacy boundaries need clearer explanations?&lt;/li&gt;
&lt;li&gt;Should a broad tool such as this remain one extension, or should some categories eventually become focused standalone products?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the first article in a series documenting the products I have built—from browser utilities to newer AI-native developer tools and desktop systems.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>chromeextension</category>
      <category>javascript</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
