Skip to content

fix(exec): isolate alias capture from rc-file stdout#418

Merged
avihut merged 1 commit intomasterfrom
fix/exec-failing-parsing-zshrc
Apr 27, 2026
Merged

fix(exec): isolate alias capture from rc-file stdout#418
avihut merged 1 commit intomasterfrom
fix/exec-failing-parsing-zshrc

Conversation

@avihut
Copy link
Copy Markdown
Owner

@avihut avihut commented Apr 27, 2026

Summary

  • daft exec (and the -x flag on daft clone / daft init / daft checkout / daft go / daft start) became unusable for users whose .zshrc/.bashrc printed anything to stdout during rc evaluation — p10k instant prompt, plugin status banners, "welcome" echoes, fortune cookies. Their stdout output was indistinguishable from real alias output and got persisted into the cached snapshot, then re-injected into every subsequent invocation.
  • Capture now writes alias/typeset -f output to dedicated temp files inside the spawned shell (paths passed via env vars). The shell's own stdout/stderr is discarded entirely, so rc-file noise can't corrupt the snapshot. A 10s deadline kills hangs from exec tmux-style rc-files. A # daft-alias-cache v2 header auto-invalidates already-poisoned caches on disk without requiring --refresh-aliases.
  • The no-cache fallback is now a pristine $SHELL -c CMD instead of $SHELL -i -c CMD. Aliases won't resolve in the fallback, but the user's command runs — strictly better than today's "completely useless" failure for users whose rc files break in non-interactive contexts.
  • run_exec_commands (the legacy -x path on worktree-creation flows) now reuses the same build_command, so the cache + rc-less fallback covers both surfaces.
  • DAFT_EXEC_DEBUG=1 emits stderr lines for chosen execution mode, capture failures, validation rejections, and cache hits — the reporter had no logs, so the fix ships its own.

Test plan

  • New unit test reproduces the bug: a polluting .bashrc previously corrupted alias_lines with POLLUTION_BEFORE_PROMPT_INIT etc.; now passes after the FD-isolated capture.
  • Manual scenario tests/manual/scenarios/worktree-exec/polluted-rcfile.yml covers the end-to-end flow.
  • End-to-end smoke: polluting .bashrc → fast-path resolves alias cleanly.
  • End-to-end smoke: SHELL=/bin/sh → rc-less fallback, command runs.
  • End-to-end smoke: planted v1 cache → header rejected, fresh capture, fresh alias used.
  • End-to-end smoke: daft init -x 'alias_name' with polluting bashrc → alias resolves through cache.
  • mise run fmt:check, mise run clippy, cargo test --lib core::worktree::exec all clean (47 tests pass).

Notes for users

  • Existing alias caches on disk are silently invalidated on first run of this build. The next invocation triggers a fresh capture; users will see a one-time alias-capture cost (the same cost as before any cache existed). No action required.
  • If shortcuts still don't resolve, run with DAFT_EXEC_DEBUG=1 and the stderr trace will show whether capture succeeded, which mode was chosen, and the constructed shell args.

🤖 Generated with Claude Code

Capture sourced the user's rc file via `$SHELL -i -c "alias -p; echo
SENTINEL; declare -f"` and split the shell's stdout on a sentinel.
Anything the rc-file printed to stdout — p10k instant prompt,
plugin status banners, "welcome" echoes, fortune cookies — was
indistinguishable from real `alias` output and got persisted into
the cached snapshot, then re-injected into every subsequent
invocation. The slow-path fallback was also `-i -c CMD`, so when
capture failed there was no escape: `daft exec` became unusable.

Capture now writes alias and function output to dedicated temp
files via env vars (`__DAFT_ALIAS_OUT`, `__DAFT_FN_OUT`); the
shell's own stdout/stderr is discarded entirely. A 10s deadline
guards against rc-files that hang. A `# daft-alias-cache v2`
header auto-invalidates already-poisoned caches on disk without
requiring `--refresh-aliases`. Captured content is sanity-checked
before persistence.

When no cache is available the fallback is now a pristine
`$SHELL -c CMD` instead of `$SHELL -i -c CMD`. Aliases won't
resolve in the fallback, but the user's command runs — strictly
better than today's "completely useless" failure for users whose
rc files break in non-interactive contexts.

`run_exec_commands` (the legacy `-x` path on `daft clone` /
`daft init` / `daft checkout` / `daft go` / `daft start`) shared
the same `-i -c` vulnerability. It now reuses `build_command` so
the cache + rc-less fallback covers that surface too.

`DAFT_EXEC_DEBUG=1` emits stderr lines for chosen execution mode,
capture failures, validation rejections, and cache hits — the user
who reported this had no logs, so the fix ships its own.

A unit test simulating a polluting `.bashrc` confirms the cache no
longer absorbs rc-stdout, and a YAML manual scenario covers the
end-to-end flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avihut avihut added this to the Public Launch milestone Apr 27, 2026
@avihut avihut added the fix Bug fix label Apr 27, 2026
@avihut avihut self-assigned this Apr 27, 2026
@avihut avihut added the fix Bug fix label Apr 27, 2026
@avihut avihut merged commit d4a4da1 into master Apr 27, 2026
8 checks passed
@avihut avihut deleted the fix/exec-failing-parsing-zshrc branch April 27, 2026 20:48
This was referenced Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant