Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,17 +330,23 @@ you pass):

## Repo-scoped GitHub auth for chezmoi

The shell config includes a `gh` wrapper that automatically sets:
`~/.config/chezmoi/profile.d/01-gh-default-config.sh` exports a profile default
`GH_CONFIG_DIR` so subprocess tools (`command gh`, `agents code-review`,
scripts) use the same authenticated config as your shell:

- `GH_CONFIG_DIR=$HOME/.config/gh-personal`
- **personal** profile: `~/.config/gh-personal`
- **work** profile: `~/.config/gh`

...when your current directory is either:
The shell `gh` wrapper still applies `gh-routes.d` and built-in repo-path
overrides on top of that default when you run `gh` interactively.

Automatic routing applies when your current directory is either:

- under `$HOME/dev/repos/github.com/aguil`, or
- inside your chezmoi source path (`chezmoi source-path`).

This lets you keep your default global `gh` account while always using your
personal profile for those trees.
This lets you keep a separate work `gh` profile while personal dotfiles work
uses `gh-personal` by default, including non-interactive subprocess callers.

Overlay profiles can extend the wrapper without replacing it by installing route
files in:
Expand Down
3 changes: 3 additions & 0 deletions docs/chezmoi-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ takes precedence.
`.chezmoi.toml.tmpl` data.
- Tooling intent metadata in `.chezmoi.toml.tmpl` (`work`: IntelliJ IDEA +
Cursor, `personal`: OpenCode + VSCode).
- GitHub CLI default config per profile via
`dot_config/chezmoi/profile.d/01-gh-default-config.sh.tmpl` (`GH_CONFIG_DIR`
for subprocess `gh`; shell `gh()` adds per-directory overrides).
- Windows-only `komorebi`/`whkd` assets via `.chezmoiignore.tmpl`.

## Optional gh auto-login via 1Password
Expand Down
11 changes: 9 additions & 2 deletions dot_agents/skills/dotfiles-github-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ source tree** that applies into it, use **`gh`** for GitHub.
## Why prefer local `gh`

It uses your login, git hosts (for example your personal GitHub SSH host alias),
and optional **`CHEZMOI_GH_HOST`** / 1Password flows from your dotfiles
docs—same context as your terminal.
profile-default `GH_CONFIG_DIR` from chezmoi `profile.d`, and optional
**`CHEZMOI_GH_HOST`** / 1Password flows from your dotfiles docs—same context as
your terminal.

Subprocess tools invoke `command gh` (not the shell `gh()` function). Chezmoi
exports `GH_CONFIG_DIR` per profile in
`~/.config/chezmoi/profile.d/01-gh-default-config.sh` so agents and scripts do
not fall back to an unauthenticated `~/.config/gh` when your interactive shell
is routed correctly.

## Multiple GitHub.com logins on one machine

Expand Down
41 changes: 19 additions & 22 deletions dot_bash_profile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export DART_FLAGS="--checked --load_deferred_eagerly"
export DARTIUM_EXPIRATION_TIME=1577836800
export GOPATH=$HOME/code/gopath
export CHEZMOI_GH_HOST='{{ if hasKey . "gh" }}{{ .gh.host }}{{ else if ne (env "CHEZMOI_GH_HOST") "" }}{{ env "CHEZMOI_GH_HOST" }}{{ else }}github.com{{ end }}'
export CHEZMOI_GH_CONFIG_DIR="${CHEZMOI_GH_CONFIG_DIR:-$HOME/.config/gh-personal}"
export AI_TERM_CMD="{{ if eq .profile "work" }}agent{{ else }}opencode{{ end }}"
path_append "$GOPATH/bin"
path_append "/usr/local/bin/nimrod/bin"
Expand Down Expand Up @@ -223,27 +222,25 @@ gh () {
local route_config_dir
local gh_personal_repo_root="$HOME/dev/repos/github.com/aguil"

if [ -z "${GH_CONFIG_DIR:-}" ]; then
if declare -F _chez_gh_route_config_dir >/dev/null 2>&1 \
&& route_config_dir="$(_chez_gh_route_config_dir)"; then
GH_CONFIG_DIR="$route_config_dir" command gh "$@"
if declare -F _chez_gh_route_config_dir >/dev/null 2>&1 \
&& route_config_dir="$(_chez_gh_route_config_dir)"; then
GH_CONFIG_DIR="$route_config_dir" command gh "$@"
return $?
fi
case "$PWD/" in
"$gh_personal_repo_root"/*|"$gh_personal_repo_root"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
fi
case "$PWD/" in
"$gh_personal_repo_root"/*|"$gh_personal_repo_root"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
;;
esac
if command -v chezmoi >/dev/null 2>&1; then
if chezmoi_source="$(chezmoi source-path 2>/dev/null)"; then
case "$PWD/" in
"$chezmoi_source"/*|"$chezmoi_source"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
;;
esac
fi
;;
esac
if command -v chezmoi >/dev/null 2>&1; then
if chezmoi_source="$(chezmoi source-path 2>/dev/null)"; then
case "$PWD/" in
"$chezmoi_source"/*|"$chezmoi_source"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
;;
esac
fi
fi

Expand All @@ -252,7 +249,7 @@ gh () {

# Agent subprocesses (Claude Code, Cursor) rehydrate shell state from export -p
# and inherit BASH_FUNC_gh without private helpers. Export the helper too.
export -f _chez_gh_route_config_dir gh 2>/dev/null || true
export -f _chez_gh_route_config_dir _chez_agent_shell_active _is_agent_shell gh 2>/dev/null || true

tmuxdev () {
local chezmoi_source
Expand Down
12 changes: 12 additions & 0 deletions dot_config/chezmoi/profile.d/00-agent-shell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ _is_agent_shell() {
[ -n "${CUSTOM_AGENT_SHELL:-}" ] && return 0
return 1
}

# Agent shells rehydrated from export -p keep env markers but may drop helper
# functions. Use this at call sites (zoxide-init) instead of _is_agent_shell alone.
_chez_agent_shell_active() {
if type _is_agent_shell >/dev/null 2>&1 && _is_agent_shell; then
return 0
fi
[ -n "${CURSOR_AGENT:-}" ] && return 0
[ "${CLAUDECODE:-}" = "1" ] && return 0
[ -n "${CUSTOM_AGENT_SHELL:-}" ] && return 0
return 1
}
15 changes: 15 additions & 0 deletions dot_config/chezmoi/profile.d/01-gh-default-config.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Default GitHub CLI config for this chezmoi profile.
#
# Subprocess tools (agents code-review, scripts, mise tasks) invoke `command gh`
# and do not run the shell gh() wrapper. Export GH_CONFIG_DIR here so they use
# the same authenticated profile as interactive shells.
#
# The gh() wrapper in dot_zshrc / dot_bash_profile still applies gh-routes.d and
# built-in repo-path overrides on top of this default.

{{- if eq .profile "work" }}
export CHEZMOI_GH_CONFIG_DIR="${CHEZMOI_GH_CONFIG_DIR:-$HOME/.config/gh}"
{{- else }}
export CHEZMOI_GH_CONFIG_DIR="${CHEZMOI_GH_CONFIG_DIR:-$HOME/.config/gh-personal}"
{{- end }}
export GH_CONFIG_DIR="${GH_CONFIG_DIR:-$CHEZMOI_GH_CONFIG_DIR}"
5 changes: 4 additions & 1 deletion dot_config/chezmoi/profile.d/zoxide-init.sh.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ chez_init_zoxide_cd() {
if [ -n "${INTELLIJ_ENVIRONMENT_READER:-}" ] || ! command -v zoxide >/dev/null 2>&1; then
return 0
fi
if type _is_agent_shell >/dev/null 2>&1 && _is_agent_shell; then
if type _chez_agent_shell_active >/dev/null 2>&1 && _chez_agent_shell_active; then
return 0
fi
if [ -n "${CURSOR_AGENT:-}" ] || [ "${CLAUDECODE:-}" = "1" ] || [ -n "${CUSTOM_AGENT_SHELL:-}" ]; then
return 0
fi

Expand Down
39 changes: 18 additions & 21 deletions dot_zshrc.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
{{- end }}
export CHEZMOI_GH_HOST='{{ if hasKey . "gh" }}{{ .gh.host }}{{ else if ne (env "CHEZMOI_GH_HOST") "" }}{{ env "CHEZMOI_GH_HOST" }}{{ else }}github.com{{ end }}'
export CHEZMOI_GH_CONFIG_DIR="${CHEZMOI_GH_CONFIG_DIR:-$HOME/.config/gh-personal}"
export AI_TERM_CMD="{{ if eq .profile "work" }}agent{{ else }}opencode{{ end }}"

{{- if eq .profile "personal" }}
Expand Down Expand Up @@ -114,27 +113,25 @@ gh () {
local route_config_dir
local gh_personal_repo_root="$HOME/dev/repos/github.com/aguil"

if [ -z "${GH_CONFIG_DIR:-}" ]; then
if whence -w _chez_gh_route_config_dir >/dev/null 2>&1 \
&& route_config_dir="$(_chez_gh_route_config_dir)"; then
GH_CONFIG_DIR="$route_config_dir" command gh "$@"
if whence -w _chez_gh_route_config_dir >/dev/null 2>&1 \
&& route_config_dir="$(_chez_gh_route_config_dir)"; then
GH_CONFIG_DIR="$route_config_dir" command gh "$@"
return $?
fi
case "$PWD/" in
"$gh_personal_repo_root"/*|"$gh_personal_repo_root"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
fi
case "$PWD/" in
"$gh_personal_repo_root"/*|"$gh_personal_repo_root"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
;;
esac
if command -v chezmoi >/dev/null 2>&1; then
if chezmoi_source="$(chezmoi source-path 2>/dev/null)"; then
case "$PWD/" in
"$chezmoi_source"/*|"$chezmoi_source"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
;;
esac
fi
;;
esac
if command -v chezmoi >/dev/null 2>&1; then
if chezmoi_source="$(chezmoi source-path 2>/dev/null)"; then
case "$PWD/" in
"$chezmoi_source"/*|"$chezmoi_source"/)
GH_CONFIG_DIR="$CHEZMOI_GH_CONFIG_DIR" command gh "$@"
return $?
;;
esac
fi
fi

Expand Down
49 changes: 49 additions & 0 deletions tests/shell/unit/agent_shell_detection.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bats

setup() {
REPO_ROOT="$(cd "${BATS_TEST_DIRNAME}/../../.." && pwd)"
PROFILE_D="$REPO_ROOT/dot_config/chezmoi/profile.d"
RENDERED_ZOXIDE="$(mktemp)"
command -v chezmoi >/dev/null 2>&1 || return 0
chezmoi execute-template \
<"$PROFILE_D/zoxide-init.sh.tmpl" >"$RENDERED_ZOXIDE"
}

teardown() {
rm -f "${RENDERED_ZOXIDE:-}"
}

@test "agent env markers skip zoxide when helper function is missing" {
command -v zoxide >/dev/null 2>&1 || skip "zoxide not installed"
command -v chezmoi >/dev/null 2>&1 || skip "chezmoi not installed"

run bash -c '
set -euo pipefail
source "$1/00-agent-shell.sh"
# shellcheck source=/dev/null
source "$2"
unset -f _is_agent_shell _chez_agent_shell_active 2>/dev/null || true
export CURSOR_AGENT=1
chez_init_zoxide_cd
declare -f cd 2>/dev/null | grep -q __zoxide && exit 1 || exit 0
' bash "$PROFILE_D" "$RENDERED_ZOXIDE"

[ "$status" -eq 0 ]
}

@test "human shell without agent markers may init zoxide cd" {
command -v zoxide >/dev/null 2>&1 || skip "zoxide not installed"
command -v chezmoi >/dev/null 2>&1 || skip "chezmoi not installed"

run bash -c '
set -euo pipefail
source "$1/00-agent-shell.sh"
# shellcheck source=/dev/null
source "$2"
unset CURSOR_AGENT CLAUDECODE CUSTOM_AGENT_SHELL
chez_init_zoxide_cd
declare -f cd 2>/dev/null | grep -q __zoxide
' bash "$PROFILE_D" "$RENDERED_ZOXIDE"

[ "$status" -eq 0 ]
}
42 changes: 42 additions & 0 deletions tests/shell/unit/gh_default_config.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bats

setup() {
REPO_ROOT="$(cd "${BATS_TEST_DIRNAME}/../../.." && pwd)"
RENDERED="$(mktemp)"
}

teardown() {
rm -f "$RENDERED"
}

render_profile_snippet() {
local profile="$1"
CHEZMOI_PROFILE="$profile" chezmoi execute-template \
<"$REPO_ROOT/dot_config/chezmoi/profile.d/01-gh-default-config.sh.tmpl" \
>"$RENDERED"
}

@test "personal profile exports gh-personal for subprocess gh" {
command -v chezmoi >/dev/null 2>&1 || skip "chezmoi not installed"
render_profile_snippet personal

run bash -c "source \"$RENDERED\"; printf '%s' \"\$GH_CONFIG_DIR\""
[ "$status" -eq 0 ]
[[ "$output" == *"/.config/gh-personal" ]]
}

@test "work profile template branch targets default gh config dir" {
grep -Fq 'eq .profile "work"' \
"$REPO_ROOT/dot_config/chezmoi/profile.d/01-gh-default-config.sh.tmpl"
grep -Fq '$HOME/.config/gh}' \
"$REPO_ROOT/dot_config/chezmoi/profile.d/01-gh-default-config.sh.tmpl"
}

@test "subprocess command gh inherits exported GH_CONFIG_DIR" {
command -v chezmoi >/dev/null 2>&1 || skip "chezmoi not installed"
render_profile_snippet personal

run bash -c "source \"$RENDERED\"; env | rg '^GH_CONFIG_DIR='"
[ "$status" -eq 0 ]
[[ "$output" == GH_CONFIG_DIR=*gh-personal ]]
}
Loading