Got personal files to ignore? Use .git/info/exclude, not the shared .gitignore

When you want Git to ignore something that's yours — a local scratch dir, editor cruft, a personal plans/ folder, machine-local config — don't add it to the tracked .gitignore. That file is shared: a rule like plans/ or wiki-drafts/ forces your personal directory conventions onto everyone, and they don't have those dirs.

Put it in .git/info/exclude instead. Same syntax as .gitignore, but it's per-clone and never committed — so it's invisible to teammates and travels with nothing:

# .git/info/exclude
.claude/settings.local.json
.claude/plans/
.claude/wiki-drafts/

The rule of thumb: **.gitignore is for what the project should ignore (build output, node_modules/, *.user); .git/info/exclude is for what you happen to ignore on this machine.** If a teammate cloning fresh wouldn't also produce the file, it doesn't belong in the shared list.

One gotcha with worktrees: info/exclude lives in the common git dir, so it's shared across all linked worktrees of the same clone — but still not across separate clones. Find it with git rev-parse --git-common-dir if you're not sure where it is.

And if you ever need to start tracking something a blanket exclude was hiding (e.g. you had .claude excluded but now want .claude/skills/ committed), narrow the exclude line to the specific paths you still want ignored — don't delete it and shove replacement rules into the shared .gitignore.

Shell scripts can't `cd` for you — source them instead

A shell script always runs in a subshell. Any cd it executes vanishes when the script exits — the calling shell's working directory is unchanged.

The fix: source the script instead of executing it. Sourcing runs the script in the current shell, so cd sticks.

alias sw='. ~/bin/sw'

Two things to update in the script itself.

First, replace every exit with return. exit in a sourced script exits the entire shell, not just the script:

exit 1  →  return 1

Second, $0 in a sourced script is the shell binary (bash), not the script path. Use ${BASH_SOURCE[0]} wherever you need the script's own location:

source "$(dirname "${BASH_SOURCE[0]}")/git-common.sh"
script_name=$(basename "${BASH_SOURCE[0]}")

The one tradeoff: functions defined inside the script leak into the shell's namespace after it returns. For a personal utility this is usually fine.

Keep secrets out of your project files — even from AI agents

When you give an AI coding agent access to your workspace, any .env file in the project is fair game. The agent will read it, and the secrets end up in conversation history, logs, or tool call outputs. The fix isn't stricter prompting — it's structural: don't put the secrets there at all.

credential-gateway is a local proxy that solves this by injecting credentials at the network level. Your application connects to localhost:8080 (or localhost:3307 for MySQL, localhost:6380 for Redis) with no credentials in the connection string. The gateway holds the real credentials in ~/.config/credential-gateway/config.yaml — outside every project worktree — and injects them before forwarding each request upstream.

The config file lives at ~/.config/credential-gateway/config.yaml (or /etc/credential-gateway/config.yaml). The gateway refuses to start if the file is group- or world-readable, so a forgotten chmod doesn't quietly undo the whole point.

# example: OpenAI-compatible HTTP proxy
proxies:
  - type: http
    listen: :8080
    target: https://api.openai.com
    auth:
      header: Authorization
      value: "Bearer sk-..."

Your app then points at http://localhost:8080 and uses an empty or placeholder token. No .env, no secrets in the repo, nothing for an agent to read.

Supported today: HTTP (OpenAI-compatible APIs), MySQL, Redis. PostgreSQL support is in progress; Oracle basic support is planned.

If you're running AI agents against a codebase that talks to external APIs or databases, this pattern is worth adopting before a key leaks rather than after.

Stuck mid-task because your Claude account hit quota? Run multiple accounts behind a local proxy

If you use Claude Code heavily, you've probably hit the quota wall mid-conversation and had to either wait or manually switch to a second account — digging up the right ANTHROPIC_AUTH_TOKEN, setting the env var, restarting. It's enough friction to break flow completely.

agent-quota-gateway is a small Go reverse proxy you run locally on 127.0.0.1. You configure your Claude Code client to point at it instead of api.anthropic.com, and it manages a pool of your accounts behind the scenes. When one hits its quota limit, the gateway automatically switches to the next one and tells the client to retry — the mid-task context survives, the switch is transparent.

# point Claude Code at the local gateway
export ANTHROPIC_BASE_URL=http://127.0.0.1:9099

# the gateway reads credentials from env vars at startup
AQG_POOL_MAIN_BACKEND_ACCT1=sk-ant-...
AQG_POOL_MAIN_BACKEND_ACCT2=sk-ant-...

A nice side effect: because the gateway does credential substitution on every request, your real OAuth tokens never travel through Claude's context. If Claude Code ever leaks what it thinks is "your token" in a tool call or a log, it's just the pool name — the actual credential stays in the gateway process.

Anthropic's weekly limit means this can't be used to multiply quota — two accounts gives you two accounts' worth of budget, no more. It's purely a quality-of-life tool for people who have multiple subscriptions and want seamless failover instead of manual juggling. The gateway won't even bind to a non-loopback address unless you explicitly enable shared mode for a Tailscale-internal network.

LightDM login bounces back to greeter after earlyoom kills dbus-daemon

When earlyoom kills dbus-daemon under memory pressure, the user's systemd instance ([email protected]) enters a broken state: all user-level sockets fail with status=219/CGROUP because the cgroup delegation context is corrupt. The next login attempt connects to /run/user/1000/bus — the socket file is still there — but nothing is listening, so dbus-update-activation-environment gets "Connection refused" and xfce4-session (or any DE) crashes immediately. LightDM sees the session exit and returns to the greeter.

The fix-display script (stop lightdm, kill stray X locks, restart lightdm) doesn't help here because the problem isn't an X lock — it's a dead D-Bus.

Diagnosis

cat ~/.xsession-errors
# dbus-update-activation-environment: error: unable to connect to D-Bus:
#   Failed to connect to socket /run/user/1000/bus: Connection refused
# /usr/bin/x-session-manager: X server already running on display :0

sudo -u davidw XDG_RUNTIME_DIR=/run/user/1000 systemctl --user list-units --failed
# dbus.service    failed
# dbus.socket     failed
# pipewire.service failed  (and everything else)

Fix

…more