Posts in category “Tips”

git can't pull private GitHub repos? gh was holding the token all along

You're logged into gh and can browse every private repo with it, but plain git pull over HTTPS still fails with fatal: could not read Username for 'https://github.com'. git doesn't know how to read gh's token store — nobody introduced them. (A GH_TOKEN env var makes every gh command work and can hide exactly this breakage, because plain git ignores it.)

You don't need Microsoft's Git Credential Manager (it drags in a .NET runtime). gh ships its own credential helper; one command wires it in:

gh auth setup-git

That adds a single stanza to your ~/.gitconfig:

[credential "https://github.com"]
    helper = !/usr/local/bin/gh auth git-credential

From then on, any HTTPS clone/pull/push of private repos authenticates through gh's stored token — global config, so it works for every repo on the machine. No SSH keys, no per-remote remote-url surgery, no token pasting.

Verify without letting an env token mask the result:

env -u GH_TOKEN git ls-remote https://github.com/<you>/<private-repo> HEAD

If that prints a ref, you're done.

codex silently ignores your custom base_url when OPENAI_API_KEY is set

We run codex CLI agents behind a local reverse proxy (pool selector, per-seat failover). The agent config looked airtight:

model_provider = "codex"

[model_providers."codex"]
base_url = "http://gateway.internal:9949"
wire_api = "responses"
requires_openai_auth = false

Yet when a seat hit its weekly cap, the agent stayed stuck on the dead seat: disabling it server-side changed nothing, only a fresh session worked. Every proxy log said the proxy was innocent. After a day of reading proxy code we put a sniffer on the client box instead — and the codex process was dialing chatgpt.com:443 directly. The config was never being used.

The trigger: the launcher exported OPENAI_API_KEY into the agent's environment. When codex sees that variable, it prefers its built-in OpenAI/ChatGPT backend and ignores the custom model_providers.*.base_url entirely — even with requires_openai_auth = false. The fix in our launcher is one line:

# codex launch env plan
unset OPENAI_API_KEY OPENAI_BASE_URL CODEX_ACCESS_TOKEN

One more trap: agents launched before the fix keep the stale env until the process actually restarts. A process's environ is frozen at exec time — check it directly with:

tr '\0' '\n' </proc/<pid>/environ | grep OPENAI_API_KEY

The general lesson: when a client "should" be going through your proxy, don't trust configs or client-side logs. Ask the kernel. Per-process sockets (ss -tnp), a tcpdump SYN filter on port 443, and DNS queries at the moment of a failing request settle in one minute what a day of log archaeology can't.

diffwalk is better as a conversation than a lecture

diffwalk's marketed flow is one-shot: your coding agent captures the diff, writes the entire walkthrough, and hands you a rendered review to read. That works for sharing, but when the goal is understanding what the agent just changed, a finished lecture is the wrong shape — you read passively, and the agent's framing never gets challenged.

Two changes made it click for me.

Publish is not part of the loop. diffwalk publish only mints a share link. For your own comprehension, diffwalk view (loopback-only) is already the end of the pipeline — and often you don't need the browser at all:

diffwalk changes            # list captured blocks
diffwalk change change-009  # read one block in the terminal

Interleave instead of pre-authoring. Instead of letting the agent write all of explanations.yaml up front, go one block at a time: the agent explains the block and gives its own take (correctness, risks, simpler alternatives), then stops and waits. I answer with mine. We decide jointly whether the block deserves a GitHub comment — the draft is shown in full and posted only after I confirm the exact text. Then, next block.

The agreed explanation for each block still lands in explanations.yaml as the loop runs, so a final diffwalk check doubles as "did we actually cover every change". The review conversation produces the walkthrough document as a side effect, instead of the document replacing the conversation.

One implementation note: I didn't edit diffwalk's own SKILL.md — it ships through npm and an update would clobber the edit. The pair-review loop lives in a separate custom skill beside it.

Want tmux on Windows without leaving your Git Bash setup? Fix MSYS2's HOME and PATH

Git Bash doesn't ship tmux. A standalone MSYS2 install does — but out of the box it ignores your Windows $HOME, doesn't inherit the Windows PATH, and if you wire it into Windows Terminal, silently ignores the config you think is fixing it.

Two settings make MSYS2 match Git Bash:

HOME — edit C:\msys64\etc\nsswitch.conf:

db_home: env windows cygwin desc
db_shell: env windows cygwin desc
db_gecos: env windows cygwin desc

Default MSYS2 uses cygwin desc, which invents a separate /home/<user> instead of your real Windows profile. This is the exact line Git for Windows ships with, so it makes MSYS2 pick up C:\Users\<you> — same .bashrc, .gitconfig, .ssh as Git Bash, no copying needed.

PATH — MSYS2 doesn't inherit the Windows PATH by default, so anything installed via scoop/chocolatey/etc. won't resolve. Set MSYS2_PATH_TYPE=inherit in the relevant launcher .ini (mingw64.ini, ucrt64.ini, next to the exe in C:\msys64).

The gotcha: if you launch MSYS2 through Windows Terminal with the commonly documented

msys2_shell.cmd -defterm -here -no-start -ucrt64

the .ini file is never read. Only the GUI launchers (mingw64.exe, ucrt64.exe) read it — msys2_shell.cmd has its own separate flag:

msys2_shell.cmd -defterm -here -no-start -ucrt64 -use-full-path

Add -use-full-path straight to the Windows Terminal profile's commandline. Or, for a fix that works no matter how MSYS2 gets launched (Windows Terminal, VS Code, a script), set it once at the system level from an elevated shell:

[Environment]::SetEnvironmentVariable('MSYS2_PATH_TYPE', 'inherit', 'Machine')

Restart the terminal for it to take effect. Then pacman -S tmux, and you've got tmux running in an environment that otherwise looks and feels exactly like Git Bash.

Baton: the harness beneath the harness

When an AI harness calls an external agent, the visible action is often just one command. The real problem starts immediately afterward: the caller may finish its turn, the tool runner may disappear, the worker may need to outlive its parent, and the reply still has to reach the right session. Pipes and background processes handle the happy path; they do not define a reliable lifecycle.

Baton is a local coordination layer for that boundary. It gives an external-agent call a mailbox and a durable message flow. A request enters an inbox, baton serve claims it, launches the configured agent, and writes the response to an outbox. Its mailbox uses atomic state transitions, single-instance locking, stale-work reclaim, and cooperative stop. Delegation becomes an explicit, inspectable protocol instead of an accidental child-process relationship.

orchestrator / harness → Baton inbox → baton serve → external agent
orchestrator / harness ← Baton outbox ← baton serve ← external agent

That is why Baton is the harness of the harness. The outer harness manages the current model turn, tools, and user interaction. Baton manages the boundary where that harness asks another agent or worker to do something. It does not replace the external model and it does not need to understand the model's reasoning. It provides the durable submission, delivery, retry, and recovery behavior that the outer harness should not have to reinvent for every provider.

The foundation is already useful, but the larger design is a service, not a collection of detached commands. A host-owned Baton supervisor can spawn each session's baton serve, persist its identity and state, and stop or reap it deliberately. That is materially stronger than setsid or disown: detachment changes a process's parent; supervision gives the process a real owner that survives the submitting client.

This is where the companion bg-run layer fits. bg-run is the agent-facing convenience: start long work and end the current turn. Baton can provide the generic task lifecycle underneath it—stable task IDs, isolated process groups, durable results, and immutable milestone or terminal events delivered back to the role mailbox. The agent receives a wake-up when there is something to consume instead of sleeping and polling.

The names should stay in their layers. bg-run describes a useful action in my-ai-team; Baton should expose provider-neutral primitives such as task or job. A future supervisor can then own both session servers and asynchronous tasks without knowing whether the caller was Codex, another harness, or a human-operated CLI.

The opportunity is bigger than a better way to launch subprocesses. If an AI harness is the harness for one agent, Baton can become the infrastructure for a whole population of delegated workers: routing their messages, preserving their work, waking their consumers, and eventually supervising their lifecycles. That is why Baton deserves to be designed as a foundation, not as a thin wrapper around tmux.