diff --git a/.chezmoiignore b/.chezmoiignore
index f21b324..525b669 100644
--- a/.chezmoiignore
+++ b/.chezmoiignore
@@ -5,6 +5,8 @@ Library/LaunchAgents/com.mgm.aws-sso-login.plist
{{ end }}
{{ if ne .machine_type "mac-mini" }}
.config/atuin-server/
+Library/LaunchAgents/dev.dotfiles.runtime-state.plist
+.local/bin/configure-runtime-state
{{ end }}
{{ if eq .machine_type "lmsh" }}
*
diff --git a/docs/specs/shell_auth_startup/BACKLOG.md b/docs/specs/shell_auth_startup/BACKLOG.md
index 53a2662..67950ef 100644
--- a/docs/specs/shell_auth_startup/BACKLOG.md
+++ b/docs/specs/shell_auth_startup/BACKLOG.md
@@ -14,3 +14,4 @@
| AUTH-010 | Deny-by-default lmsh source target boundary | 2026-07-25 | `4fdc25e` |
| AUTH-010-session-credentials | Session-isolated GCP credentials with stale-file rematerialization | 2026-08-01 | `a8e0d9a` |
| AUTH-011-cache-relocation | Routed supported CLI caches and Pulumi home through sentinel-guarded native paths; paused PyCharm, Prefect, and Codex | 2026-08-09 | `712e31e` |
+| AUTH-013-runtime-state-relocation | Routed validated Prefect, Codex, and PyCharm state externally with team-safe defaults and runtime fallback | 2026-08-09 | `0e3e9dc` |
diff --git a/docs/specs/shell_auth_startup/CHANGELOG.md b/docs/specs/shell_auth_startup/CHANGELOG.md
index 21ebceb..a208cde 100644
--- a/docs/specs/shell_auth_startup/CHANGELOG.md
+++ b/docs/specs/shell_auth_startup/CHANGELOG.md
@@ -2,6 +2,20 @@
## 2026-08-09
+- Routed validated Prefect and Codex homes through their native
+ controls and PyCharm system state through custom properties. All settings are
+ Mac-mini-only, sentinel-guarded, and excluded from teammate defaults.
+- Preserved 802 Prefect runs; copied databases passed SQLite integrity checks,
+ Prefect server health, Codex GUI file-handle checks, and PyCharm control
+ restarts. A login controller reconciles GUI state at load, sentinel changes,
+ and 60-second intervals while preserving unrelated inherited values.
+- Deployed the shell profile and LaunchAgent, then moved internal rollback copies
+ to `/Volumes/ext/state/archive/runtime-state-rollback-20260809`. Post-move
+ activation remained external and reclaimed about 12 GiB internally.
+- Validation: 173 tests, Bash/Zsh syntax, plist/render checks, SQLite integrity,
+ launchd state, and post-move GUI/runtime smoke passed. Accepted risk:
+ `AUTH-013-AR1`. Implementation Gate Commit: `0e3e9dc`.
+
- Routed Playwright, uv, pre-commit, npm, and Pulumi through their native path
controls only when the existing external-state sentinel is present. Inherited
managed values clear on the fallback path without overriding unrelated values.
diff --git a/docs/specs/shell_auth_startup/README.md b/docs/specs/shell_auth_startup/README.md
index cec7713..dac6aca 100644
--- a/docs/specs/shell_auth_startup/README.md
+++ b/docs/specs/shell_auth_startup/README.md
@@ -335,10 +335,15 @@ Glossary:
- **Invariant:** Playwright, uv, pre-commit, and npm use only their
documented native path controls; shell-wide `XDG_CACHE_HOME` and cache
symlinks are not used.
-- **Invariant:** Pulumi uses its documented `PULUMI_HOME`; Prefect databases and
- Codex session state remain internal until their installed runtimes can pass
- migration integrity tests. PyCharm remains internal until its external paths
- can fail back when the volume is unavailable.
+- **Invariant:** Pulumi, Prefect, and Codex use their documented native home
+ controls only on the Mac mini with the external-state sentinel present.
+- **Invariant:** Codex GUI processes receive the same native `CODEX_HOME` through
+ the Mac-mini-only login environment; missing-sentinel startup removes only the
+ managed value.
+- **Invariant:** PyCharm settings and plugins remain internal; its regenerable
+ system directory uses `idea.system.path` only on the Mac mini.
+- **Pre:** migrated Prefect and Codex SQLite files pass integrity and runtime
+ activation checks before internal rollback copies may be removed.
- **Invariant:** `TerminalTargetAllowlist` denies all targets by default and
re-includes only `.bash_profile`, `.bashrc`, `.common_profile`,
`.config/atuin/config.toml`, `.config/starship.toml`, and
@@ -371,6 +376,14 @@ Glossary:
- **Post:** remote failure does not prevent local history search or capture.
### Accepted Risk
+- `AUTH-013-AR1`: on 2026-08-09 the operator approved storing Prefect history and
+ Codex authentication/session state, and PyCharm Local History on the existing
+ unencrypted, `noowners` external volume after each exposure was reported.
+ Sentinel fallback, validated copies, and physical custody reduce availability
+ and migration risk but do not provide encryption or local ownership isolation.
+ Owner: operator. Review before the volume leaves trusted custody, another local
+ account gains access, or the storage policy changes; encrypt the volume when
+ operationally feasible.
- `AUTH-011-AR1`: the operator approved moving Pulumi credentials and executable
caches to the existing unencrypted, `noowners` external state volume after the
trust limitation was reported. The state sentinel, retained internal rollback
@@ -389,7 +402,12 @@ Glossary:
- Shell syntax checks pass for edited scripts.
- Rendered Mac mini shell startup selects external CLI caches only with the
sentinel present; absent-sentinel startup retains local defaults.
-- PyCharm custom properties remain absent after the attempted migration rollback.
+- Prefect preserves 802 runs, passes SQLite `quick_check`, and serves a healthy
+ local API from the external home.
+- Codex CLI and GUI use the external home; copied SQLite databases pass
+ `quick_check` and live file handles resolve externally.
+- PyCharm control restart opens the external system directory without internal
+ cache file handles.
- Static search confirms no Herdr profile auto-`secret` block remains.
- Static search confirms Clockify poller has no `op read` call.
- Static search confirms Databricks config has no `onepasswordRead` call.
diff --git a/dot_common_profile.tmpl b/dot_common_profile.tmpl
index a77a02f..6f70e72 100644
--- a/dot_common_profile.tmpl
+++ b/dot_common_profile.tmpl
@@ -106,12 +106,18 @@ if [ -f "/Volumes/ext/state/.dotfiles-ai-state" ]; then
export PRE_COMMIT_HOME="$DOTFILES_CACHE_ROOT/pre-commit"
export npm_config_cache="$DOTFILES_CACHE_ROOT/npm"
export PULUMI_HOME="/Volumes/ext/state/pulumi"
+ export PREFECT_HOME="/Volumes/ext/state/prefect/seo-data-science"
+ export CODEX_HOME="/Volumes/ext/state/codex/home"
+ export PYCHARM_PROPERTIES="/Volumes/ext/state/jetbrains/PyCharm2026.2/idea.properties"
else
[ "${PLAYWRIGHT_BROWSERS_PATH:-}" = "/Volumes/ext/state/cache/playwright" ] && unset PLAYWRIGHT_BROWSERS_PATH
[ "${UV_CACHE_DIR:-}" = "/Volumes/ext/state/cache/uv" ] && unset UV_CACHE_DIR
[ "${PRE_COMMIT_HOME:-}" = "/Volumes/ext/state/cache/pre-commit" ] && unset PRE_COMMIT_HOME
[ "${npm_config_cache:-}" = "/Volumes/ext/state/cache/npm" ] && unset npm_config_cache
[ "${PULUMI_HOME:-}" = "/Volumes/ext/state/pulumi" ] && unset PULUMI_HOME
+ [ "${PREFECT_HOME:-}" = "/Volumes/ext/state/prefect/seo-data-science" ] && unset PREFECT_HOME
+ [ "${CODEX_HOME:-}" = "/Volumes/ext/state/codex/home" ] && unset CODEX_HOME
+ [ "${PYCHARM_PROPERTIES:-}" = "/Volumes/ext/state/jetbrains/PyCharm2026.2/idea.properties" ] && unset PYCHARM_PROPERTIES
[ "${DOTFILES_CACHE_ROOT:-}" = "/Volumes/ext/state/cache" ] && unset DOTFILES_CACHE_ROOT
fi
{{ end -}}
diff --git a/dot_local/bin/executable_configure-runtime-state b/dot_local/bin/executable_configure-runtime-state
new file mode 100644
index 0000000..2df4a8b
--- /dev/null
+++ b/dot_local/bin/executable_configure-runtime-state
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+codex_home=/Volumes/ext/state/codex/home
+pycharm_properties=/Volumes/ext/state/jetbrains/PyCharm2026.2/idea.properties
+if [ -f /Volumes/ext/state/.dotfiles-ai-state ]; then
+ /bin/launchctl setenv CODEX_HOME "$codex_home"
+ /bin/launchctl setenv PYCHARM_PROPERTIES "$pycharm_properties"
+else
+ [ "$(/bin/launchctl getenv CODEX_HOME)" = "$codex_home" ] && /bin/launchctl unsetenv CODEX_HOME
+ [ "$(/bin/launchctl getenv PYCHARM_PROPERTIES)" = "$pycharm_properties" ] && /bin/launchctl unsetenv PYCHARM_PROPERTIES
+fi
+exit 0
diff --git a/private_Library/LaunchAgents/dev.dotfiles.runtime-state.plist.tmpl b/private_Library/LaunchAgents/dev.dotfiles.runtime-state.plist.tmpl
new file mode 100644
index 0000000..9302244
--- /dev/null
+++ b/private_Library/LaunchAgents/dev.dotfiles.runtime-state.plist.tmpl
@@ -0,0 +1,20 @@
+
+
+
+
+ Label
+ dev.dotfiles.runtime-state
+ ProgramArguments
+
+ {{ .chezmoi.homeDir }}/.local/bin/configure-runtime-state
+
+ RunAtLoad
+
+ WatchPaths
+
+ /Volumes/ext/state/.dotfiles-ai-state
+
+ StartInterval
+ 60
+
+
diff --git a/tests/test_terminal_environment.py b/tests/test_terminal_environment.py
index 28bfcc9..b44e96c 100644
--- a/tests/test_terminal_environment.py
+++ b/tests/test_terminal_environment.py
@@ -11,6 +11,9 @@ def text(path):
def test_lmsh_profile_is_portable_and_excludes_credentials():
+ assert '"machine_type" "Machine type (macbook/mac-mini/lmsh)" "macbook"' in text(
+ ".chezmoi.toml.tmpl"
+ )
assert "macbook/mac-mini/lmsh" in text(".chezmoi.toml.tmpl")
assert "atuin_sync_address" in text(".chezmoi.toml.tmpl")
ignored = text(".chezmoiignore")
@@ -39,6 +42,9 @@ def test_mac_mini_uses_native_external_cache_paths():
'PRE_COMMIT_HOME="$DOTFILES_CACHE_ROOT/pre-commit"',
'npm_config_cache="$DOTFILES_CACHE_ROOT/npm"',
'PULUMI_HOME="/Volumes/ext/state/pulumi"',
+ 'PREFECT_HOME="/Volumes/ext/state/prefect/seo-data-science"',
+ 'CODEX_HOME="/Volumes/ext/state/codex/home"',
+ 'PYCHARM_PROPERTIES="/Volumes/ext/state/jetbrains/PyCharm2026.2/idea.properties"',
):
assert setting in profile
for variable in (
@@ -48,6 +54,9 @@ def test_mac_mini_uses_native_external_cache_paths():
"PRE_COMMIT_HOME",
"npm_config_cache",
"PULUMI_HOME",
+ "PREFECT_HOME",
+ "CODEX_HOME",
+ "PYCHARM_PROPERTIES",
):
assert f'unset {variable}' in profile
@@ -69,6 +78,9 @@ def test_external_cache_exports_and_fallback(tmp_path):
"PRE_COMMIT_HOME": "/Volumes/ext/state/cache/pre-commit",
"npm_config_cache": "/Volumes/ext/state/cache/npm",
"PULUMI_HOME": "/Volumes/ext/state/pulumi",
+ "PREFECT_HOME": "/Volumes/ext/state/prefect/seo-data-science",
+ "CODEX_HOME": "/Volumes/ext/state/codex/home",
+ "PYCHARM_PROPERTIES": "/Volumes/ext/state/jetbrains/PyCharm2026.2/idea.properties",
}
shells = [shell for shell in ("/bin/bash", "/bin/zsh") if Path(shell).exists()]
@@ -91,6 +103,62 @@ def test_external_cache_exports_and_fallback(tmp_path):
assert f"{name}=" not in fallback
+def test_mac_mini_gui_state_paths_are_scoped_and_native():
+ ignored = text(".chezmoiignore")
+ mac_mini = ignored.split('{{ if ne .machine_type "mac-mini" }}', 1)[1].split("{{ end }}", 1)[0]
+ assert "Library/LaunchAgents/dev.dotfiles.runtime-state.plist" in mac_mini
+ assert ".local/bin/configure-runtime-state" in mac_mini
+ runtime_state = text("dot_local/bin/executable_configure-runtime-state")
+ assert "/Volumes/ext/state/.dotfiles-ai-state" in runtime_state
+ assert "codex_home=/Volumes/ext/state/codex/home" in runtime_state
+ assert "pycharm_properties=/Volumes/ext/state/jetbrains/PyCharm2026.2/idea.properties" in runtime_state
+ for variable in ("CODEX_HOME", "PYCHARM_PROPERTIES"):
+ assert f"/bin/launchctl setenv {variable}" in runtime_state
+ assert f"/bin/launchctl unsetenv {variable}" in runtime_state
+ launch_agent = text("private_Library/LaunchAgents/dev.dotfiles.runtime-state.plist.tmpl")
+ assert "WatchPaths" in launch_agent
+ assert "StartInterval" in launch_agent
+
+
+def test_gui_state_controller_tracks_sentinel_without_overwriting_custom_values(tmp_path):
+ sentinel = tmp_path / "sentinel"
+ state = tmp_path / "launchctl-state"
+ state.mkdir()
+ launchctl = tmp_path / "launchctl"
+ launchctl.write_text(
+ """#!/bin/sh
+case "$1" in
+ setenv) printf '%s' "$3" > "$LAUNCHCTL_STATE/$2" ;;
+ getenv) cat "$LAUNCHCTL_STATE/$2" 2>/dev/null ;;
+ unsetenv) rm -f "$LAUNCHCTL_STATE/$2" ;;
+esac
+"""
+ )
+ launchctl.chmod(0o755)
+ script = (
+ text("dot_local/bin/executable_configure-runtime-state")
+ .replace("/Volumes/ext/state/.dotfiles-ai-state", str(sentinel))
+ .replace("/bin/launchctl", str(launchctl))
+ )
+ env = os.environ | {"LAUNCHCTL_STATE": str(state)}
+
+ sentinel.touch()
+ subprocess.run(["/bin/sh", "-c", script], env=env, check=True)
+ assert (state / "CODEX_HOME").read_text() == "/Volumes/ext/state/codex/home"
+ assert (state / "PYCHARM_PROPERTIES").read_text() == (
+ "/Volumes/ext/state/jetbrains/PyCharm2026.2/idea.properties"
+ )
+
+ sentinel.unlink()
+ subprocess.run(["/bin/sh", "-c", script], env=env, check=True)
+ assert not list(state.iterdir())
+
+ for variable in ("CODEX_HOME", "PYCHARM_PROPERTIES"):
+ (state / variable).write_text("/custom")
+ subprocess.run(["/bin/sh", "-c", script], env=env, check=True)
+ assert {path.read_text() for path in state.iterdir()} == {"/custom"}
+
+
def test_lmsh_targets_are_deny_by_default():
ignored = text(".chezmoiignore")
lmsh = ignored.split('{{ if eq .machine_type "lmsh" }}', 1)[1].split("{{ end }}", 1)[0]