Skip to content

Commit db2f96b

Browse files
domenkozarclaude
andcommitted
Add devenv hook for native directory-based auto-activation (#2488)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b5c9a1c commit db2f96b

File tree

14 files changed

+686
-8
lines changed

14 files changed

+686
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
- Added Ctrl+X keybinding to stop individual processes from the TUI while keeping them visible and restartable.
2222
- Tasks can now display messages when entering the shell by writing `{"devenv":{"messages":["..."]}}` to `$DEVENV_TASK_OUTPUT_FILE` ([#2500](https://github.com/cachix/devenv/issues/2500)).
23+
- Added `devenv hook <shell>` for native directory based auto-activation without direnv. Supports bash, zsh, fish, and nushell. Automatically deactivates when you leave the project directory. Add `eval "$(devenv hook bash)"` to your shell config to activate. Use `devenv allow` and `devenv revoke` to manage trust ([#2488](https://github.com/cachix/devenv/issues/2488)).
2324
- Added `nixpkgs.rocmSupport` option to enable ROCm support in nixpkgs configuration.
2425
- Added process management subcommands and MCP tools: `devenv processes list`, `status`, `logs`, `restart`, `start`, `stop` for interacting with running native processes ([#2621](https://github.com/cachix/devenv/issues/2621)).
2526
## 2.0.6 (2026-03-22)

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.nix

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
# This file was @generated by crate2nix 0.15.0 with the command:
3-
# "generate" "-h" "nix/crate-hashes.json"
3+
# "generate"
44
# See https://github.com/kolloch/crate2nix for more info.
55

66
{ nixpkgs ? <nixpkgs>
@@ -6089,6 +6089,10 @@ rec {
60896089
name = "devenv-eval-cache";
60906090
packageId = "devenv-eval-cache";
60916091
}
6092+
{
6093+
name = "devenv-event-sources";
6094+
packageId = "devenv-event-sources";
6095+
}
60926096
{
60936097
name = "devenv-nix-backend";
60946098
packageId = "devenv-nix-backend";
@@ -6182,6 +6186,10 @@ rec {
61826186
name = "pathdiff";
61836187
packageId = "pathdiff";
61846188
}
6189+
{
6190+
name = "portable-pty";
6191+
packageId = "portable-pty";
6192+
}
61856193
{
61866194
name = "rand";
61876195
packageId = "rand 0.10.0";

devenv/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ devenv-processes.workspace = true
1717
devenv-tasks.workspace = true
1818
devenv-tui.workspace = true
1919
devenv-cache-core.workspace = true
20+
devenv-event-sources.workspace = true
2021
devenv-reload.workspace = true
2122
devenv-shell.workspace = true
23+
portable-pty.workspace = true
2224
devenv-snix-backend = { workspace = true, optional = true }
2325
devenv-nix-backend.workspace = true
2426
rand.workspace = true

devenv/hook-fish.fish

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# devenv hook for fish
2+
# Usage: devenv hook fish | source
3+
4+
set -g _DEVENV_HOOK_LAST_PROJECT ""
5+
set -g _DEVENV_HOOK_UNTRUSTED ""
6+
7+
function _devenv_hook --on-variable PWD
8+
# Inside devenv shell: exit when leaving the project directory
9+
if set -q DEVENV_ROOT
10+
switch $PWD
11+
case "$DEVENV_ROOT" "$DEVENV_ROOT/*"
12+
return
13+
case '*'
14+
exit
15+
end
16+
end
17+
18+
# stderr flows through so user sees the "not allowed" message
19+
set -l project_dir (devenv hook-should-activate --last "$_DEVENV_HOOK_LAST_PROJECT")
20+
set -l exit_code $status
21+
22+
if test $exit_code -eq 0 -a -n "$project_dir"
23+
set -lx _DEVENV_HOOK_DIR $project_dir
24+
fish -c 'cd -- $_DEVENV_HOOK_DIR; and devenv shell'
25+
set -g _DEVENV_HOOK_LAST_PROJECT $project_dir
26+
set -g _DEVENV_HOOK_UNTRUSTED ""
27+
else if test $exit_code -ne 0
28+
# Untrusted; retry silently on each prompt until allowed
29+
set -g _DEVENV_HOOK_UNTRUSTED $PWD
30+
set -g _DEVENV_HOOK_LAST_PROJECT ""
31+
else
32+
set -g _DEVENV_HOOK_LAST_PROJECT ""
33+
set -g _DEVENV_HOOK_UNTRUSTED ""
34+
end
35+
end
36+
37+
function _devenv_hook_prompt --on-event fish_prompt
38+
# Retry activation for untrusted directories after 'devenv allow'
39+
if test -z "$_DEVENV_HOOK_UNTRUSTED"
40+
return
41+
end
42+
# Inside devenv shell: no retry needed
43+
if set -q DEVENV_ROOT
44+
return
45+
end
46+
47+
set -l project_dir (devenv hook-should-activate --last "$_DEVENV_HOOK_LAST_PROJECT" 2>/dev/null)
48+
if test $status -eq 0 -a -n "$project_dir"
49+
set -lx _DEVENV_HOOK_DIR $project_dir
50+
fish -c 'cd -- $_DEVENV_HOOK_DIR; and devenv shell'
51+
set -g _DEVENV_HOOK_LAST_PROJECT $project_dir
52+
set -g _DEVENV_HOOK_UNTRUSTED ""
53+
end
54+
end
55+
56+
# Trigger initial check
57+
if test -n "$PWD"
58+
_devenv_hook
59+
end

devenv/hook-nu.nu

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# devenv hook for nushell
2+
# Usage: Add to your config.nu:
3+
# source (devenv hook nu | save --force ~/.cache/devenv/hook.nu; "~/.cache/devenv/hook.nu")
4+
# Or: devenv hook nu | save --force ~/.cache/devenv/hook.nu
5+
# source ~/.cache/devenv/hook.nu
6+
7+
$env._DEVENV_HOOK_UNTRUSTED = ""
8+
9+
$env.config = ($env.config | upsert hooks.env_change.PWD (
10+
($env.config | get -i hooks.env_change.PWD | default []) | append {||
11+
# Inside devenv shell: exit when leaving the project directory
12+
if ("DEVENV_ROOT" in $env) {
13+
if not ($env.PWD == $env.DEVENV_ROOT or ($env.PWD | str starts-with ($env.DEVENV_ROOT + "/"))) {
14+
exit
15+
}
16+
return
17+
}
18+
19+
let last = ($env | get -i _DEVENV_HOOK_LAST_PROJECT | default "")
20+
let result = (^devenv hook-should-activate --last $last | complete)
21+
22+
if ($result.stderr | str trim) != "" {
23+
print -e $result.stderr
24+
}
25+
26+
if $result.exit_code == 0 {
27+
let dir = ($result.stdout | str trim)
28+
if $dir != "" {
29+
do { cd $dir; ^devenv shell }
30+
$env._DEVENV_HOOK_LAST_PROJECT = $dir
31+
$env._DEVENV_HOOK_UNTRUSTED = ""
32+
} else {
33+
$env._DEVENV_HOOK_LAST_PROJECT = ""
34+
$env._DEVENV_HOOK_UNTRUSTED = ""
35+
}
36+
} else {
37+
$env._DEVENV_HOOK_LAST_PROJECT = ""
38+
$env._DEVENV_HOOK_UNTRUSTED = $env.PWD
39+
}
40+
}
41+
))
42+
43+
# Retry activation on each prompt for untrusted directories (after 'devenv allow')
44+
$env.config = ($env.config | upsert hooks.pre_prompt (
45+
($env.config | get -i hooks.pre_prompt | default []) | append {||
46+
let untrusted = ($env | get -i _DEVENV_HOOK_UNTRUSTED | default "")
47+
if $untrusted == "" {
48+
return
49+
}
50+
if ("DEVENV_ROOT" in $env) {
51+
return
52+
}
53+
54+
let last = ($env | get -i _DEVENV_HOOK_LAST_PROJECT | default "")
55+
let result = (^devenv hook-should-activate --last $last | complete)
56+
57+
if $result.exit_code == 0 {
58+
let dir = ($result.stdout | str trim)
59+
if $dir != "" {
60+
do { cd $dir; ^devenv shell }
61+
$env._DEVENV_HOOK_LAST_PROJECT = $dir
62+
$env._DEVENV_HOOK_UNTRUSTED = ""
63+
}
64+
}
65+
}
66+
))

devenv/hook-posix.sh

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# devenv hook shared function for bash/zsh
2+
3+
_DEVENV_HOOK_PWD=""
4+
_DEVENV_HOOK_LAST_PROJECT=""
5+
_DEVENV_HOOK_UNTRUSTED=""
6+
7+
_devenv_hook() {
8+
local previous_exit_status=$?
9+
[[ "$_DEVENV_HOOK_PWD" == "$PWD" ]] && return $previous_exit_status
10+
11+
# Inside devenv shell: exit when leaving the project directory
12+
if [[ -n "${DEVENV_ROOT:-}" ]]; then
13+
case "$PWD" in
14+
"${DEVENV_ROOT}"|"${DEVENV_ROOT}"/*) ;;
15+
*) exit $previous_exit_status ;;
16+
esac
17+
_DEVENV_HOOK_PWD="$PWD"
18+
return $previous_exit_status
19+
fi
20+
21+
# Already showed untrusted message for this directory; silently retry
22+
if [[ "$_DEVENV_HOOK_UNTRUSTED" == "$PWD" ]]; then
23+
local project_dir
24+
project_dir=$(devenv hook-should-activate --last "${_DEVENV_HOOK_LAST_PROJECT:-}" 2>/dev/null)
25+
if [[ $? -eq 0 && -n "$project_dir" ]]; then
26+
_DEVENV_HOOK_PWD="$PWD"
27+
_DEVENV_HOOK_UNTRUSTED=""
28+
(cd "$project_dir" && devenv shell)
29+
_DEVENV_HOOK_LAST_PROJECT="$project_dir"
30+
fi
31+
return $previous_exit_status
32+
fi
33+
34+
local project_dir exit_code
35+
project_dir=$(devenv hook-should-activate --last "${_DEVENV_HOOK_LAST_PROJECT:-}")
36+
exit_code=$?
37+
38+
if [[ $exit_code -eq 0 && -n "$project_dir" ]]; then
39+
_DEVENV_HOOK_PWD="$PWD"
40+
_DEVENV_HOOK_UNTRUSTED=""
41+
(cd "$project_dir" && devenv shell)
42+
_DEVENV_HOOK_LAST_PROJECT="$project_dir"
43+
elif [[ $exit_code -eq 0 ]]; then
44+
# No project or already activated; cache to avoid rechecking
45+
_DEVENV_HOOK_PWD="$PWD"
46+
_DEVENV_HOOK_UNTRUSTED=""
47+
_DEVENV_HOOK_LAST_PROJECT=""
48+
else
49+
# Untrusted project; message already printed to stderr, suppress on retry
50+
_DEVENV_HOOK_UNTRUSTED="$PWD"
51+
_DEVENV_HOOK_LAST_PROJECT=""
52+
fi
53+
return $previous_exit_status
54+
}

devenv/src/cli.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,29 @@ pub enum Commands {
593593
print_config: bool,
594594
},
595595

596+
#[command(
597+
about = "Print shell hook for auto-activation on directory change.",
598+
long_about = "Print shell hook for auto-activation on directory change.\n\nAdd to your shell config:\n\n bash: eval \"$(devenv hook bash)\" # in ~/.bashrc\n zsh: eval \"$(devenv hook zsh)\" # in ~/.zshrc\n fish: devenv hook fish | source # in ~/.config/fish/config.fish\n nushell: see devenv hook nu # in config.nu"
599+
)]
600+
Hook {
601+
#[arg(value_enum)]
602+
shell: HookShell,
603+
},
604+
605+
#[command(about = "Allow auto-activation for the current directory.")]
606+
Allow,
607+
608+
#[command(about = "Revoke auto-activation for the current directory.")]
609+
Revoke,
610+
611+
/// Internal: check if hook should activate devenv in current directory
612+
#[clap(hide = true)]
613+
HookShouldActivate {
614+
/// Last activated project directory (to prevent re-entry)
615+
#[arg(long)]
616+
last: Option<String>,
617+
},
618+
596619
/// Internal: run native process manager as a daemon (used by `devenv up -d`)
597620
#[clap(hide = true)]
598621
DaemonProcesses {
@@ -601,6 +624,14 @@ pub enum Commands {
601624
},
602625
}
603626

627+
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
628+
pub enum HookShell {
629+
Bash,
630+
Zsh,
631+
Fish,
632+
Nu,
633+
}
634+
604635
#[derive(clap::Args, Clone, Debug)]
605636
pub struct UpArgs {
606637
#[arg(help = "Start a specific process(es).")]

0 commit comments

Comments
 (0)