Posts tagged with “tech_note”

Git alias could run in dash, not bash — even if `/bin/bash` exists

I Wrote a git alias using [[ ... ]] and =~. Works fine in my interactive bash. Run the alias and it explodes:

$ git co master
... Syntax error: "(" unexpected (expecting "then")

First instinct: "but I have bash installed":

$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1298416 ... /bin/bash
$ /bin/bash --version
GNU bash, version 5.2.37(1)-release ...

Doesn't matter. A git alias starting with ! is hardcoded to run under /bin/sh — it doesn't read $SHELL, doesn't care what your login shell is. On Debian/Ubuntu /bin/sh -> dash, and dash doesn't understand [[, =~, == or other bash extensions:

$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4 ... /bin/sh -> dash
$ echo '[[ "a" == a* ]]' | /bin/sh
/bin/sh: 1: [[: not found

The source is run-command.c::prepare_shell_cmd() in git itself — it literally calls sh -c.

Two fixes:

Wrap in bash -c explicitly (minimal change, but the nested quoting inside an alias gets ugly fast):

co = "!bash -c 'f() { ...bash syntax... }; f \"$@\"' _"

Rewrite as POSIX sh (preferred). Common substitutions:

  • [[ "$x" == -* ]]case "$x" in -*) ... ;; esac
  • [[ "$x" =~ ^HEAD~ ]]case "$x" in HEAD~*) ... ;; esac
  • [[ "$a" == "$b" ]][ "$a" = "$b" ]
  • [[ -f foo ]][ -f foo ] (already POSIX, no reason to use [[)

Lesson: write git aliases as if dash is the only shell on the planet. Don't reach for [[ because it feels nicer — either wrap with bash -c or just use case.

Auto-run startup scripts in Oracle 19c Docker to keep disk clean

Oracle's diagnostic files (trace, incident dumps, listener logs) grow without bound inside the container. If you're running doctorkirk/oracle-19c locally, they'll silently eat your disk.

The fix: mount a startup script into /opt/oracle/scripts/startup/ — this directory runs on every container start (unlike /docker-entrypoint-initdb.d/ which only runs on first database creation).

# docker-compose.yml
volumes:
  - ./startup-scripts:/opt/oracle/scripts/startup
# startup-scripts/init.sh (chmod +x)
#!/bin/bash
echo "=== Oracle startup config starting ==="

# Set ADR purge policies — auto-delete old diagnostic files
adrci <<EOF
set home diag/rdbms/orcl/ORCL
set control (SHORTP_POLICY = 24)
set control (LONGP_POLICY = 48)
set home diag/tnslsnr/$(hostname)/listener
set control (SHORTP_POLICY = 24)
set control (LONGP_POLICY = 48)
EOF
echo "ADR purge policy set: SHORT=24h, LONG=48h"

# Disable listener log (it's huge and rarely needed locally)
lsnrctl set log_status off
echo "Listener logging disabled"

echo "=== Oracle startup config done ==="

SHORTP_POLICY controls how long short-lived files (trace, cdump) are kept. LONGP_POLICY controls incident dumps and core files. 24h/48h is aggressive but fine for a dev environment.

Add echo markers so you can verify it ran: docker compose logs oracle19c | grep "startup config".

One more thing — the diag directory lives in the container's writable layer by default. If Oracle goes haywire (e.g. ORA-600 loop), it can still fill up overlay2 and tank Docker entirely. Mount it out as a safety valve:

- ./oracle-19c/diag/:/opt/oracle/diag

tmux mouse selection not copying to clipboard? Bind MouseDragEnd + MouseUp

With set -g mouse on in tmux, dragging to select text enters copy mode but the selection stays trapped inside tmux — it never reaches the system clipboard. The fix is a two-line binding that pipes the selection through xclip:

# ~/.tmux.conf
set -g mouse on

# Auto-copy on mouse drag end
bind -T copy-mode MouseDragEnd1Pane send -X copy-pipe-and-cancel "xclip -selection clipboard"
# Fallback: also copy on simple mouse-up (covers cases where drag is too short)
bind -T copy-mode MouseUp1Pane send -X copy-pipe-and-cancel "xclip -selection clipboard"

You need xclip installed (sudo apt install xclip). Reload with tmux source-file ~/.tmux.conf.

The copy-pipe-and-cancel does three things at once: grabs the selection, pipes it to xclip -selection clipboard, then exits copy mode (which clears the highlight). That's how you know it worked — the highlight disappearing means the text is in your clipboard.

Why two bindings

MouseDragEnd1Pane only fires on a proper drag. If you click-and-release too quickly, tmux enters copy mode but the drag-end event never fires, leaving the highlight stuck. MouseUp1Pane catches those cases.

When it won't work

If a pane is running an application that captures mouse input (vim, htop, some CLI tools like GitHub Copilot), tmux forwards mouse events to that program and the bindings don't fire. Your fallback in those panes is holding Shift while selecting — that bypasses tmux entirely and uses the terminal emulator's native selection, but it crosses pane boundaries so the text may include content from adjacent panes.

Setting `CLAUDE_CONFIG_DIR` in `.env`? It silently loses to your shell.

Trying to isolate a child process by setting an env var in .env, only to find the value inherited from your shell wins instead. dotenv by default does not overwrite existing process.env values.

This bit me in HelloEDi. I wanted to spawn claude -p with CLAUDE_CONFIG_DIR pointing at an isolated dir so the user's global ~/.claude/CLAUDE.md (which carries a personal persona) wouldn't leak into the assistant. .env had:

CLAUDE_CONFIG_DIR=/home/davidw/.helloedi/claude-home

But my shell already exports CLAUDE_CONFIG_DIR=/home/davidw/.claudeh for my own Claude Code setup. dotenv loaded .env, saw the var already set, did nothing. The server's process.env.CLAUDE_CONFIG_DIR was still the shell's value. The spawned claude happily read the persona from there. The assistant introduced itself with the wrong name.

The fix is to never put the canonical var name in .env when your dev environment is likely to export it. Use a project-internal name and translate on spawn:

# .env
EDI_CLAUDE_CONFIG_DIR=/home/davidw/.helloedi/claude-home
// claudeRunner.js
const dir = process.env.EDI_CLAUDE_CONFIG_DIR ?? defaultDir;
spawn('claude', args, {
  env: { ...process.env, CLAUDE_CONFIG_DIR: dir },
});

Now the shell's CLAUDE_CONFIG_DIR stays in process.env (harmless to the parent), .env's EDI_* value is read without collision, and the child gets the right CLAUDE_CONFIG_DIR because we set it explicitly in the spawn env.

dotenv does have { override: true }, but flipping it globally affects every variable in .env — including ones you actually want the shell to win on. The renamed-variable approach is scoped and self-documenting.

The shape of this trap is general: whenever a name in .env overlaps with one your shell may already export, the child sees whichever value won at parent startup, not the one you wrote. Pick names you control, or be explicit at spawn time.

Your .bashrc bails out before agents see PATH — fix it with BASH_ENV

The first non-comment line in most .bashrc files is this:

[ -z "$PS1" ] && return

It means: if this shell isn't interactive, do nothing. Innocent-looking, but it silently breaks any agent that shells out via bash -c '...' — the child shell gets no PATH additions, no cargo env, no API keys you carefully exported in .bashrc.custom. Your terminal works fine, your CLI agents don't, and the failure mode is some bare command name "not found" that has nothing to do with the actual problem.

The fix is to split the file by purpose (env vs interactive), not by file (.bashrc vs .bashrc.custom), and use BASH_ENV as the loader for non-interactive shells.

Split env from interactive

Pull every export, every PATH= mutation, every . ~/.cargo/env and . ~/.bashrc.secret out of .bashrc.custom and put them in a new file. Call it ~/.bashrc.env. It must be idempotent and produce no output (no echo, no which).

# ~/.bashrc.env — env-only, safe for both interactive and non-interactive shells.
export FNM_DIR="$HOME/.local/share/fnm"
export PNPM_HOME="$HOME/.local/share/pnpm"

case ":$PATH:" in
  *":$FNM_DIR:"*) ;;
  *) PATH="$FNM_DIR:$PATH" ;;
esac
# ... more PATH cases ...

export PATH

[ -f "$HOME/.cargo/env" ]      && . "$HOME/.cargo/env"
[ -f "$HOME/.bashrc.secret" ]  && . "$HOME/.bashrc.secret"

# This is the lever — child non-interactive bashes will source this file.
export BASH_ENV="$HOME/.bashrc.env"
…more