diff --git a/cmd/failsafe/main.go b/cmd/failsafe/main.go
index e0a7edc..a6ae540 100644
--- a/cmd/failsafe/main.go
+++ b/cmd/failsafe/main.go
@@ -42,10 +42,16 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return subcommand.MCP(stdin, stdout, stderr)
case "explain":
cwd, _ := os.Getwd()
+ // Resolve the live mode the same way the hook does, so explain's
+ // verdict matches what the hook would actually decide.
+ liveMode, _, err := subcommand.ModeChainFromConfig(cfg, os.Getenv("HOME")).Resolve(subcommand.EnvFromOS())
+ if err != nil {
+ liveMode = "enabled" // fail-safe: an unresolvable mode never weakens the verdict
+ }
return subcommand.Explain(args[2:], stdout, subcommand.ExplainOptions{
Home: os.Getenv("HOME"),
CWD: cwd,
- Mode: "enabled", // chain default is always hardcoded; no mode.default config key
+ Mode: liveMode,
})
case "test":
if len(args) < 3 {
diff --git a/cmd/failsafe/main_test.go b/cmd/failsafe/main_test.go
index 1ce0cfe..f4d363f 100644
--- a/cmd/failsafe/main_test.go
+++ b/cmd/failsafe/main_test.go
@@ -43,3 +43,20 @@ func TestRun_Report(t *testing.T) {
t.Errorf("report did not include the logged kubectl decision:\n%s", stdout.String())
}
}
+
+// Regression: explain must resolve the live mode chain (env, pane files, …),
+// not assume "enabled". A disabled pane must be reflected in explain's verdict,
+// or the docs claim "explain shows what the hook would do" is false.
+func TestRun_ExplainResolvesLiveMode(t *testing.T) {
+ t.Setenv("HOME", t.TempDir()) // isolate pane/config state
+ t.Setenv("FAILSAFE_MODE", "disabled")
+
+ var stdout, stderr bytes.Buffer
+ code := run([]string{"failsafe", "explain", "kubectl delete ns payments"}, nil, &stdout, &stderr)
+ if code != 0 && code != 1 { // 0=allow, 1=block are both valid exits; 2=usage error is not
+ t.Fatalf("exit code = %d; stderr=%s", code, stderr.String())
+ }
+ if !strings.Contains(stdout.String(), "Mode: disabled") {
+ t.Errorf("explain ignored FAILSAFE_MODE=disabled:\n%s", stdout.String())
+ }
+}
diff --git a/docs/assets/casts/audit.cast b/docs/assets/casts/audit.cast
index e0c871b..c2122d0 100644
--- a/docs/assets/casts/audit.cast
+++ b/docs/assets/casts/audit.cast
@@ -1,29 +1,58 @@
-{"version":2,"width":92,"height":22,"timestamp":1784331898}
-[0.005935, "o", "\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n"]
-[0.006175, "o", "\u001b[?1034h"]
-[0.006191, "o", "bash-3.2$ "]
-[0.013003, "i", "export PS1='$ '; cd \"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/project\"; clear\r"]
-[0.013056, "o", "e"]
-[0.013092, "o", "xport PS1='$ '; "]
-[0.013099, "o", "cd"]
-[0.013206, "o", " \"/var/folders/12/mydd4m455zn9t"]
-[0.013214, "o", "__"]
-[0.013315, "o", "48syck4940000gn/T//fa"]
-[0.013368, "o", "ilsafe-do \rcs-"]
-[0.013432, "o", "home/project\"; clear\r\n"]
-[0.015773, "o", "\u001b[3J\u001b[H\u001b[2J"]
-[0.016056, "o", "$ "]
-[0.543033, "i", "failsafe audit\r"]
-[0.543163, "o", "f"]
-[0.543232, "o", "ailsafe audit\r\n"]
-[0.557055, "o", "Policy layers in effect at /var/folders/12/mydd4m455zn9t__48syck4940000gn/T/failsafe-docs-home/project:\r\n\r\n [bundled]\r\n"]
-[0.557309, "o", " bundled/aws.rego — 2 block rule(s)\r\n"]
-[0.557386, "o", " bundled/failsafe.rego — 2 block rule(s)\r\n"]
-[0.557536, "o", " bundled/git.rego — 0 block rule(s)\r\n"]
-[0.557630, "o", " bundled/helm.rego — 2 block rule(s)\r\n"]
-[0.557722, "o", " bundled/kubectl.rego — 1 block rule(s)\r\n"]
-[0.557803, "o", " bundled/terraform.rego — 2 block rule(s)\r\n\r\n9 block rules total, 0 allow_override.\r\n"]
-[0.558949, "o", "$ "]
-[2.585943, "i", "exit\r"]
-[2.586019, "o", "ex"]
-[2.586093, "o", "it\r\nexit\r\n"]
+{"version":2,"width":92,"height":22,"timestamp":1784334849}
+[0.005793, "o", "\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n"]
+[0.006007, "o", "\u001b[?1034h"]
+[0.006020, "o", "bash-3.2$ "]
+[0.011097, "i", "export PS1='$ '; cd \"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/project\"; clear\r"]
+[0.011128, "o", "e"]
+[0.011165, "o", "xport PS1='$ '; cd "]
+[0.011213, "o", "\"/var/folders/12/my"]
+[0.011260, "o", "dd4m455zn9t__4"]
+[0.011267, "o", "8s"]
+[0.011343, "o", "yck4940000gn/T//fai"]
+[0.011386, "o", "lsafe-do \rcs-ho"]
+[0.011428, "o", "me/project\"; clea"]
+[0.011469, "o", "r\r\n"]
+[0.013415, "o", "\u001b[3J\u001b[H\u001b[2J"]
+[0.013650, "o", "$ "]
+[0.536898, "i", "f"]
+[0.537003, "o", "f"]
+[0.612068, "i", "a"]
+[0.612155, "o", "a"]
+[0.683702, "i", "i"]
+[0.683783, "o", "i"]
+[0.748184, "i", "l"]
+[0.748229, "o", "l"]
+[0.815811, "i", "s"]
+[0.815869, "o", "s"]
+[0.884732, "i", "a"]
+[0.884803, "o", "a"]
+[0.951374, "i", "f"]
+[0.951433, "o", "f"]
+[1.025238, "i", "e"]
+[1.025380, "o", "e"]
+[1.090788, "i", " "]
+[1.090838, "o", " "]
+[1.159001, "i", "a"]
+[1.159063, "o", "a"]
+[1.226645, "i", "u"]
+[1.226720, "o", "u"]
+[1.296252, "i", "d"]
+[1.296308, "o", "d"]
+[1.367866, "i", "i"]
+[1.367924, "o", "i"]
+[1.437083, "i", "t"]
+[1.437144, "o", "t"]
+[1.507146, "i", "\r"]
+[1.507203, "o", "\r\n"]
+[1.516153, "o", "Policy layers in effect at /var/folders/12/mydd4m455zn9t__48syck4940000gn/T/failsafe-docs-home/project:\r\n\r\n [bundled]\r\n"]
+[1.516374, "o", " bundled/aws.rego — 2 block rule(s)\r\n"]
+[1.516422, "o", " bundled/failsafe.rego — 2 block rule(s)\r\n"]
+[1.516444, "o", " bundled/git.rego — 0 block rule(s)\r\n"]
+[1.516514, "o", " bundled/helm.rego — 2 block rule(s)\r\n"]
+[1.516592, "o", " bundled/kubectl.rego — 1 block rule(s)\r\n"]
+[1.516675, "o", " bundled/terraform.rego — 2 block rule(s)\r\n\r\n9 block rules total, 0 allow_override.\r\n"]
+[1.517401, "o", "$ "]
+[3.536809, "i", "exit\r"]
+[3.536926, "o", "e"]
+[3.536943, "o", "xit\r\n"]
+[3.536986, "o", "exit\r\n"]
diff --git a/docs/assets/casts/audit.svg b/docs/assets/casts/audit.svg
index 9455ff9..e8218eb 100644
--- a/docs/assets/casts/audit.svg
+++ b/docs/assets/casts/audit.svg
@@ -1 +1 @@
-
\ No newline at end of file
+Thedefaultinteractiveshellisnowzsh.Toupdateyouraccounttousezsh,pleaserun`chsh-s/bin/zsh`.Formoredetails,pleasevisithttps://support.apple.com/kb/HT208050.bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-do$$failsafe$failsafeauditPolicylayersineffectat/var/folders/12/mydd4m455zn9t__48syck4940000gn/T/failsafe-docs-home/project:[bundled]bundled/aws.rego—2blockrule(s)bundled/failsafe.rego—2blockrule(s)bundled/git.rego—0blockrule(s)bundled/helm.rego—2blockrule(s)bundled/kubectl.rego—1blockrule(s)bundled/terraform.rego—2blockrule(s)9blockrulestotal,0allow_override.$exitbash-3.2$bash-3.2$ebash-3.2$exportPS1='$';cdbash-3.2$exportPS1='$';cd"/var/folders/12/mybash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__4bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48sbash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//faics-hocs-home/project";cleacs-home/project";clear$f$fa$fai$fail$fails$failsa$failsaf$failsafea$failsafeau$failsafeaud$failsafeaudi$eexit
\ No newline at end of file
diff --git a/docs/assets/casts/explain-block.cast b/docs/assets/casts/explain-block.cast
index 04aa745..73760b7 100644
--- a/docs/assets/casts/explain-block.cast
+++ b/docs/assets/casts/explain-block.cast
@@ -1,33 +1,231 @@
-{"version":2,"width":92,"height":22,"timestamp":1784331884}
-[0.007669, "o", "\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n"]
-[0.010914, "o", "\u001b[?1034h"]
-[0.011090, "o", "bash-3.2$ "]
-[0.016704, "i", "export PS1='$ '; cd \"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/project\"; clear\r"]
-[0.016757, "o", "e"]
-[0.016839, "o", "xport PS1='$ '; cd \"/var/fol"]
-[0.016850, "o", "de"]
-[0.016962, "o", "rs/12/mydd4m455zn9t__48sy"]
-[0.017024, "o", "ck4940000gn/"]
-[0.017032, "o", "T"]
-[0.017127, "o", "//failsafe-do \rcs-hom"]
-[0.017198, "o", "e/project\"; clear\r\n"]
-[0.021285, "o", "\u001b[3J\u001b[H\u001b[2J"]
-[0.021574, "o", "$ "]
-[0.544513, "i", "failsafe explain \"kubectl --context arn:aws:eks:us-east-1:123456789012:cluster/prod delete ns payments\"\r"]
-[0.544583, "o", "f"]
-[0.544639, "o", "ailsafe explain \"k"]
-[0.544713, "o", "ubectl --context "]
-[0.544726, "o", "ar"]
-[0.544847, "o", "n:aws:eks:us-east-1:"]
-[0.544859, "o", "12"]
-[0.544984, "o", "3456789012:cluste"]
-[0.545062, "o", "r/prod de"]
-[0.545137, "o", "lete \r ns pa"]
-[0.545211, "o", "yments\"\r\n"]
-[0.848167, "o", "── call 1: kubectl ──\r\nVerb: delete\r\nFlags:\r\n"]
-[0.848216, "o", " context = arn:aws:eks:us-east-1:123456789012:cluster/prod\r\nPositional: ns payments\r\nEffective cwd: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T/failsafe-docs-home/project\r\nMode: enabled\r\nPolicy chain (6 modules at this cwd):\r\n [bundled] bundled/aws.rego\r\n [bundled] bundled/failsafe.rego\r\n [bundled] bundled/git.rego\r\n [bundled] bundled/helm.rego\r\n [bundled] bundled/kubectl.rego\r\n [bundled] bundled/terraform.rego\r\nDecision: BLOCK\r\nReason : kubectl delete blocked while failsafe is enabled\r\n"]
-[0.849408, "o", "$ "]
-[2.882566, "i", "exit\r"]
-[2.882745, "o", "e"]
-[2.882788, "o", "xit\r\n"]
-[2.882877, "o", "exit\r\n"]
+{"version":2,"width":92,"height":22,"timestamp":1784334823}
+[0.004300, "o", "\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n"]
+[0.004605, "o", "\u001b[?1034hbash-3.2$ "]
+[0.008887, "i", "export PS1='$ '; cd \"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/project\"; clear\r"]
+[0.008944, "o", "e"]
+[0.008984, "o", "xport PS1='$ '; cd \"/var/f"]
+[0.008993, "o", "olde"]
+[0.009079, "o", "rs/12/mydd4m455zn9t__48syck494000"]
+[0.009084, "o", "0"]
+[0.009086, "o", "g"]
+[0.009089, "o", "n"]
+[0.009151, "o", "/T//failsafe-do \rcs-hom"]
+[0.009185, "o", "e/project\"; clea"]
+[0.009218, "o", "r\r\n"]
+[0.011101, "o", "\u001b[3J\u001b[H\u001b[2J"]
+[0.011305, "o", "$ "]
+[0.524566, "i", "f"]
+[0.524653, "o", "f"]
+[0.591133, "i", "a"]
+[0.591185, "o", "a"]
+[0.656933, "i", "i"]
+[0.656977, "o", "i"]
+[0.722965, "i", "l"]
+[0.723013, "o", "l"]
+[0.790270, "i", "s"]
+[0.790322, "o", "s"]
+[0.856334, "i", "a"]
+[0.856379, "o", "a"]
+[0.923842, "i", "f"]
+[0.923894, "o", "f"]
+[0.991235, "i", "e"]
+[0.991287, "o", "e"]
+[1.059449, "i", " "]
+[1.059506, "o", " "]
+[1.127136, "i", "e"]
+[1.127196, "o", "e"]
+[1.193988, "i", "x"]
+[1.194055, "o", "x"]
+[1.262359, "i", "p"]
+[1.262430, "o", "p"]
+[1.326483, "i", "l"]
+[1.326541, "o", "l"]
+[1.389784, "i", "a"]
+[1.389837, "o", "a"]
+[1.454047, "i", "i"]
+[1.454088, "o", "i"]
+[1.519117, "i", "n"]
+[1.519183, "o", "n"]
+[1.583495, "i", " "]
+[1.583541, "o", " "]
+[1.652557, "i", "\""]
+[1.652629, "o", "\""]
+[1.716813, "i", "k"]
+[1.716867, "o", "k"]
+[1.784570, "i", "u"]
+[1.784624, "o", "u"]
+[1.851673, "i", "b"]
+[1.851730, "o", "b"]
+[1.918567, "i", "e"]
+[1.918627, "o", "e"]
+[1.985452, "i", "c"]
+[1.985498, "o", "c"]
+[2.052410, "i", "t"]
+[2.052458, "o", "t"]
+[2.122370, "i", "l"]
+[2.122432, "o", "l"]
+[2.195485, "i", " "]
+[2.195563, "o", " "]
+[2.264328, "i", "-"]
+[2.264417, "o", "-"]
+[2.331462, "i", "-"]
+[2.331506, "o", "-"]
+[2.399034, "i", "c"]
+[2.399087, "o", "c"]
+[2.467399, "i", "o"]
+[2.467456, "o", "o"]
+[2.537710, "i", "n"]
+[2.537804, "o", "n"]
+[2.605700, "i", "t"]
+[2.605770, "o", "t"]
+[2.672689, "i", "e"]
+[2.672768, "o", "e"]
+[2.739452, "i", "x"]
+[2.739503, "o", "x"]
+[2.803879, "i", "t"]
+[2.803934, "o", "t"]
+[2.869406, "i", " "]
+[2.869495, "o", " "]
+[2.935837, "i", "a"]
+[2.935885, "o", "a"]
+[2.997366, "i", "r"]
+[2.997416, "o", "r"]
+[3.061840, "i", "n"]
+[3.061893, "o", "n"]
+[3.129341, "i", ":"]
+[3.129401, "o", ":"]
+[3.193124, "i", "a"]
+[3.193181, "o", "a"]
+[3.261219, "i", "w"]
+[3.261268, "o", "w"]
+[3.327434, "i", "s"]
+[3.327482, "o", "s"]
+[3.393364, "i", ":"]
+[3.393411, "o", ":"]
+[3.461350, "i", "e"]
+[3.461409, "o", "e"]
+[3.532832, "i", "k"]
+[3.532901, "o", "k"]
+[3.606090, "i", "s"]
+[3.606175, "o", "s"]
+[3.679299, "i", ":"]
+[3.679369, "o", ":"]
+[3.747239, "i", "u"]
+[3.747334, "o", "u"]
+[3.814713, "i", "s"]
+[3.814752, "o", "s"]
+[3.880518, "i", "-"]
+[3.880563, "o", "-"]
+[3.947592, "i", "e"]
+[3.947646, "o", "e"]
+[4.017352, "i", "a"]
+[4.017422, "o", "a"]
+[4.089702, "i", "s"]
+[4.089797, "o", "s"]
+[4.161091, "i", "t"]
+[4.161155, "o", "t"]
+[4.228802, "i", "-"]
+[4.228885, "o", "-"]
+[4.293712, "i", "1"]
+[4.293764, "o", "1"]
+[4.359526, "i", ":"]
+[4.359571, "o", ":"]
+[4.425429, "i", "1"]
+[4.425486, "o", "1"]
+[4.491568, "i", "2"]
+[4.491625, "o", "2"]
+[4.558319, "i", "3"]
+[4.558377, "o", "3"]
+[4.625343, "i", "4"]
+[4.625400, "o", "4"]
+[4.694108, "i", "5"]
+[4.694167, "o", "5"]
+[4.759564, "i", "6"]
+[4.759628, "o", "6"]
+[4.826793, "i", "7"]
+[4.826856, "o", "7"]
+[4.893609, "i", "8"]
+[4.893661, "o", "8"]
+[4.960664, "i", "9"]
+[4.960722, "o", "9"]
+[5.028871, "i", "0"]
+[5.028926, "o", "0"]
+[5.092111, "i", "1"]
+[5.092171, "o", "1"]
+[5.160811, "i", "2"]
+[5.160867, "o", "2"]
+[5.228150, "i", ":"]
+[5.228215, "o", ":"]
+[5.293785, "i", "c"]
+[5.293837, "o", "c"]
+[5.360933, "i", "l"]
+[5.360987, "o", "l"]
+[5.428940, "i", "u"]
+[5.428997, "o", "u"]
+[5.497721, "i", "s"]
+[5.497793, "o", "s"]
+[5.569537, "i", "t"]
+[5.569617, "o", "t"]
+[5.642320, "i", "e"]
+[5.642397, "o", "e"]
+[5.709363, "i", "r"]
+[5.709454, "o", "r"]
+[5.780519, "i", "/"]
+[5.780602, "o", "/"]
+[5.846083, "i", "p"]
+[5.846127, "o", "p"]
+[5.913533, "i", "r"]
+[5.913647, "o", "r"]
+[5.982615, "i", "o"]
+[5.982689, "o", "o"]
+[6.049044, "i", "d"]
+[6.049102, "o", "d"]
+[6.115504, "i", " "]
+[6.115558, "o", " "]
+[6.184849, "i", "d"]
+[6.184899, "o", "d"]
+[6.248980, "i", "e"]
+[6.249038, "o", "e"]
+[6.315944, "i", "l"]
+[6.316003, "o", "l"]
+[6.383705, "i", "e"]
+[6.383810, "o", "e"]
+[6.449221, "i", "t"]
+[6.449295, "o", "t"]
+[6.512343, "i", "e"]
+[6.512405, "o", "e \r"]
+[6.575826, "i", " "]
+[6.575866, "o", " "]
+[6.640099, "i", "n"]
+[6.640141, "o", "n"]
+[6.704344, "i", "s"]
+[6.704387, "o", "s"]
+[6.771080, "i", " "]
+[6.771125, "o", " "]
+[6.838123, "i", "p"]
+[6.838198, "o", "p"]
+[6.902998, "i", "a"]
+[6.903051, "o", "a"]
+[6.967990, "i", "y"]
+[6.968050, "o", "y"]
+[7.030989, "i", "m"]
+[7.031029, "o", "m"]
+[7.094314, "i", "e"]
+[7.094368, "o", "e"]
+[7.157317, "i", "n"]
+[7.157362, "o", "n"]
+[7.222577, "i", "t"]
+[7.222625, "o", "t"]
+[7.288976, "i", "s"]
+[7.289036, "o", "s"]
+[7.352101, "i", "\""]
+[7.352148, "o", "\""]
+[7.413834, "i", "\r"]
+[7.413885, "o", "\r\n"]
+[7.703114, "o", "── call 1: kubectl ──\r\nVerb: delete\r\nFlags:\r\n"]
+[7.703163, "o", " context = arn:aws:eks:us-east-1:123456789012:cluster/prod\r\nPositional: ns payments\r\nEffective cwd: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T/failsafe-docs-home/project\r\nMode: enabled\r\nPolicy chain (6 modules at this cwd):\r\n [bundled] bundled/aws.rego\r\n [bundled] bundled/failsafe.rego\r\n [bundled] bundled/git.rego\r\n [bundled] bundled/helm.rego\r\n [bundled] bundled/kubectl.rego\r\n [bundled] bundled/terraform.rego\r\nDecision: BLOCK\r\nReason : kubectl delete blocked while failsafe is enabled\r\n"]
+[7.703842, "o", "$ "]
+[9.727401, "i", "exit\r"]
+[9.727520, "o", "e"]
+[9.727581, "o", "xit\r\n"]
+[9.727634, "o", "exit\r\n"]
diff --git a/docs/assets/casts/explain-block.svg b/docs/assets/casts/explain-block.svg
index 5caab03..8241dac 100644
--- a/docs/assets/casts/explain-block.svg
+++ b/docs/assets/casts/explain-block.svg
@@ -1 +1 @@
-Thedefaultinteractiveshellisnowzsh.Toupdateyouraccounttousezsh,pleaserun`chsh-s/bin/zsh`.Formoredetails,pleasevisithttps://support.apple.com/kb/HT208050.bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-do$$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/proddeletenspayments"──call1:kubectl──Verb:deleteFlags:context=arn:aws:eks:us-east-1:123456789012:cluster/prodPositional:nspaymentsEffectivecwd:/var/folders/12/mydd4m455zn9t__48syck4940000gn/T/failsafe-docs-home/projectMode:enabledPolicychain(6modulesatthiscwd):[bundled]bundled/aws.rego[bundled]bundled/failsafe.rego[bundled]bundled/git.rego[bundled]bundled/helm.rego[bundled]bundled/kubectl.rego[bundled]bundled/terraform.regoDecision:BLOCKReason:kubectldeleteblockedwhilefailsafeisenabled$exitbash-3.2$bash-3.2$ebash-3.2$exportPS1='$';cd"/var/folbash-3.2$exportPS1='$';cd"/var/foldebash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48sybash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/Tcs-homcs-home/project";clear$f$failsafeexplain"k$failsafeexplain"kubectl--context$failsafeexplain"kubectl--contextar$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:12$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluste$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/proddenspa$eexit
\ No newline at end of file
+Thedefaultinteractiveshellisnowzsh.Toupdateyouraccounttousezsh,pleaserun`chsh-s/bin/zsh`.Formoredetails,pleasevisithttps://support.apple.com/kb/HT208050.bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-do$$failsafe$failsafeexplain$failsafeexplain"kubectl$failsafeexplain"kubectl--context$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/prod$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/proddeletensnspayments"──call1:kubectl──Verb:deleteFlags:context=arn:aws:eks:us-east-1:123456789012:cluster/prodPositional:nspaymentsEffectivecwd:/var/folders/12/mydd4m455zn9t__48syck4940000gn/T/failsafe-docs-home/projectMode:enabledPolicychain(6modulesatthiscwd):[bundled]bundled/aws.rego[bundled]bundled/failsafe.rego[bundled]bundled/git.rego[bundled]bundled/helm.rego[bundled]bundled/kubectl.rego[bundled]bundled/terraform.regoDecision:BLOCKReason:kubectldeleteblockedwhilefailsafeisenabled$exitbash-3.2$bash-3.2$ebash-3.2$exportPS1='$';cd"/var/fbash-3.2$exportPS1='$';cd"/var/foldebash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck494000bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gbash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gncs-homcs-home/project";cleacs-home/project";clear$f$fa$fai$fail$fails$failsa$failsaf$failsafee$failsafeex$failsafeexp$failsafeexpl$failsafeexpla$failsafeexplai$failsafeexplain"$failsafeexplain"k$failsafeexplain"ku$failsafeexplain"kub$failsafeexplain"kube$failsafeexplain"kubec$failsafeexplain"kubect$failsafeexplain"kubectl-$failsafeexplain"kubectl--$failsafeexplain"kubectl--c$failsafeexplain"kubectl--co$failsafeexplain"kubectl--con$failsafeexplain"kubectl--cont$failsafeexplain"kubectl--conte$failsafeexplain"kubectl--contex$failsafeexplain"kubectl--contexta$failsafeexplain"kubectl--contextar$failsafeexplain"kubectl--contextarn$failsafeexplain"kubectl--contextarn:$failsafeexplain"kubectl--contextarn:a$failsafeexplain"kubectl--contextarn:aw$failsafeexplain"kubectl--contextarn:aws$failsafeexplain"kubectl--contextarn:aws:$failsafeexplain"kubectl--contextarn:aws:e$failsafeexplain"kubectl--contextarn:aws:ek$failsafeexplain"kubectl--contextarn:aws:eks$failsafeexplain"kubectl--contextarn:aws:eks:$failsafeexplain"kubectl--contextarn:aws:eks:u$failsafeexplain"kubectl--contextarn:aws:eks:us$failsafeexplain"kubectl--contextarn:aws:eks:us-$failsafeexplain"kubectl--contextarn:aws:eks:us-e$failsafeexplain"kubectl--contextarn:aws:eks:us-ea$failsafeexplain"kubectl--contextarn:aws:eks:us-eas$failsafeexplain"kubectl--contextarn:aws:eks:us-east$failsafeexplain"kubectl--contextarn:aws:eks:us-east-$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:1$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:12$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:1234$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:12345$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:1234567$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:12345678$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:1234567890$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:12345678901$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:c$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cl$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:clu$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:clus$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:clust$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluste$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/p$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/pr$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/pro$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/prodd$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/prodde$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/proddel$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/proddele$failsafeexplain"kubectl--contextarn:aws:eks:us-east-1:123456789012:cluster/proddeletnnspnspanspaynspaymnspaymenspaymennspaymentnspayments$eexit
\ No newline at end of file
diff --git a/docs/assets/casts/mode-toggle.cast b/docs/assets/casts/mode-toggle.cast
index 76a3cb8..e447205 100644
--- a/docs/assets/casts/mode-toggle.cast
+++ b/docs/assets/casts/mode-toggle.cast
@@ -1,43 +1,206 @@
-{"version":2,"width":92,"height":22,"timestamp":1784331888}
-[0.005927, "o", "\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n"]
-[0.006178, "o", "\u001b[?1034h"]
-[0.006212, "o", "bash-3.2$ "]
-[0.014081, "i", "export PS1='$ '; cd \"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/project\"; clear\r"]
-[0.014288, "o", "e"]
-[0.014310, "o", "xport PS1='$"]
-[0.014358, "o", " '; cd \"/var/folde"]
-[0.014367, "o", "rs/"]
-[0.014445, "o", "12/mydd4m455zn9t__48sy"]
-[0.014452, "o", "ck"]
-[0.014535, "o", "4940000gn/T//failsa"]
-[0.014578, "o", "fe-do \rcs-home/"]
-[0.014585, "o", "pr"]
-[0.014589, "o", "oj"]
-[0.014664, "o", "ect\"; clear\r\n"]
-[0.016854, "o", "\u001b[3J\u001b[H\u001b[2J"]
-[0.017117, "o", "$ "]
-[0.539295, "i", "failsafe mode get\r"]
-[0.539375, "o", "f"]
-[0.539437, "o", "ailsafe mode get\r\n"]
-[0.550818, "o", "enabled\t(default; no source resolved)\r\n"]
-[0.551704, "o", "$ "]
-[2.583063, "i", "failsafe mode set disabled\r"]
-[2.583205, "o", "f"]
-[2.583281, "o", "ailsafe mode set disabled\r\n"]
-[2.598676, "o", "disabled\t(file: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)\r\n"]
-[2.599698, "o", "$ "]
-[4.614719, "i", "failsafe mode get\r"]
-[4.614865, "o", "f"]
-[4.614953, "o", "ailsafe mode get\r\n"]
-[4.629334, "o", "disabled\t(file: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)\r\n"]
-[4.630511, "o", "$ "]
-[7.160456, "i", "failsafe mode set enabled\r"]
-[7.160543, "o", "f"]
-[7.160604, "o", "ailsafe mode set e"]
-[7.160689, "o", "nabled\r\n"]
-[7.171934, "o", "enabled\t(file: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)\r\n"]
-[7.172671, "o", "$ "]
-[9.711063, "i", "exit\r"]
-[9.711225, "o", "e"]
-[9.711286, "o", "xit\r\n"]
-[9.711402, "o", "exit\r\n"]
+{"version":2,"width":92,"height":22,"timestamp":1784334833}
+[0.004162, "o", "\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n"]
+[0.004318, "o", "\u001b[?1034h"]
+[0.004333, "o", "bash-3.2$ "]
+[0.009875, "i", "export PS1='$ '; cd \"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/project\"; clear\r"]
+[0.009920, "o", "e"]
+[0.009950, "o", "xport PS1='$ '; c"]
+[0.009995, "o", "d \"/var/folders/12/"]
+[0.010042, "o", "mydd4m455zn9t__4"]
+[0.010085, "o", "8syck4940000g"]
+[0.010091, "o", "n"]
+[0.010164, "o", "/T//failsafe-do \rcs-home"]
+[0.010202, "o", "/project\"; clear"]
+[0.010207, "o", "\r\n"]
+[0.012249, "o", "\u001b[3J\u001b[H\u001b[2J"]
+[0.012475, "o", "$ "]
+[0.526209, "i", "f"]
+[0.526296, "o", "f"]
+[0.592558, "i", "a"]
+[0.592641, "o", "a"]
+[0.658105, "i", "i"]
+[0.658153, "o", "i"]
+[0.724168, "i", "l"]
+[0.724223, "o", "l"]
+[0.789577, "i", "s"]
+[0.789634, "o", "s"]
+[0.855260, "i", "a"]
+[0.855314, "o", "a"]
+[0.922138, "i", "f"]
+[0.922206, "o", "f"]
+[0.988933, "i", "e"]
+[0.988987, "o", "e"]
+[1.053316, "i", " "]
+[1.053420, "o", " "]
+[1.121810, "i", "m"]
+[1.121878, "o", "m"]
+[1.188858, "i", "o"]
+[1.188922, "o", "o"]
+[1.255934, "i", "d"]
+[1.255992, "o", "d"]
+[1.325474, "i", "e"]
+[1.325555, "o", "e"]
+[1.391403, "i", " "]
+[1.391456, "o", " "]
+[1.458447, "i", "g"]
+[1.458499, "o", "g"]
+[1.525156, "i", "e"]
+[1.525207, "o", "e"]
+[1.591756, "i", "t"]
+[1.591810, "o", "t"]
+[1.658382, "i", "\r"]
+[1.658438, "o", "\r\n"]
+[1.666953, "o", "enabled\t(default; no source resolved)\r\n"]
+[1.667733, "o", "$ "]
+[3.685959, "i", "f"]
+[3.686031, "o", "f"]
+[3.755600, "i", "a"]
+[3.755668, "o", "a"]
+[3.827533, "i", "i"]
+[3.827606, "o", "i"]
+[3.899667, "i", "l"]
+[3.899764, "o", "l"]
+[3.973322, "i", "s"]
+[3.973384, "o", "s"]
+[4.043884, "i", "a"]
+[4.043966, "o", "a"]
+[4.110717, "i", "f"]
+[4.110759, "o", "f"]
+[4.177252, "i", "e"]
+[4.177299, "o", "e"]
+[4.240126, "i", " "]
+[4.240180, "o", " "]
+[4.305944, "i", "m"]
+[4.306012, "o", "m"]
+[4.373124, "i", "o"]
+[4.373186, "o", "o"]
+[4.439670, "i", "d"]
+[4.439725, "o", "d"]
+[4.503856, "i", "e"]
+[4.503917, "o", "e"]
+[4.571240, "i", " "]
+[4.571315, "o", " "]
+[4.639508, "i", "s"]
+[4.639558, "o", "s"]
+[4.707252, "i", "e"]
+[4.707309, "o", "e"]
+[4.777386, "i", "t"]
+[4.777470, "o", "t"]
+[4.845633, "i", " "]
+[4.845698, "o", " "]
+[4.911548, "i", "d"]
+[4.911590, "o", "d"]
+[4.977796, "i", "i"]
+[4.977848, "o", "i"]
+[5.045858, "i", "s"]
+[5.045931, "o", "s"]
+[5.114947, "i", "a"]
+[5.115010, "o", "a"]
+[5.183756, "i", "b"]
+[5.183822, "o", "b"]
+[5.252126, "i", "l"]
+[5.252182, "o", "l"]
+[5.320527, "i", "e"]
+[5.320577, "o", "e"]
+[5.388325, "i", "d"]
+[5.388390, "o", "d"]
+[5.453116, "i", "\r"]
+[5.453165, "o", "\r\n"]
+[5.461886, "o", "disabled\t(file: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)\r\n"]
+[5.462535, "o", "$ "]
+[7.476508, "i", "f"]
+[7.476576, "o", "f"]
+[7.540666, "i", "a"]
+[7.540724, "o", "a"]
+[7.604941, "i", "i"]
+[7.605006, "o", "i"]
+[7.671109, "i", "l"]
+[7.671161, "o", "l"]
+[7.738290, "i", "s"]
+[7.738343, "o", "s"]
+[7.804500, "i", "a"]
+[7.804550, "o", "a"]
+[7.871701, "i", "f"]
+[7.871748, "o", "f"]
+[7.935776, "i", "e"]
+[7.935824, "o", "e"]
+[8.003051, "i", " "]
+[8.003149, "o", " "]
+[8.069881, "i", "m"]
+[8.069950, "o", "m"]
+[8.137047, "i", "o"]
+[8.137101, "o", "o"]
+[8.202301, "i", "d"]
+[8.202354, "o", "d"]
+[8.269111, "i", "e"]
+[8.269161, "o", "e"]
+[8.338715, "i", " "]
+[8.338779, "o", " "]
+[8.409166, "i", "g"]
+[8.409240, "o", "g"]
+[8.473000, "i", "e"]
+[8.473175, "o", "e"]
+[8.536607, "i", "t"]
+[8.536657, "o", "t"]
+[8.596941, "i", "\r"]
+[8.596980, "o", "\r\n"]
+[8.602675, "o", "disabled\t(file: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)\r\n"]
+[8.603223, "o", "$ "]
+[11.122598, "i", "f"]
+[11.122682, "o", "f"]
+[11.189778, "i", "a"]
+[11.189839, "o", "a"]
+[11.255421, "i", "i"]
+[11.255477, "o", "i"]
+[11.321706, "i", "l"]
+[11.321794, "o", "l"]
+[11.388673, "i", "s"]
+[11.388732, "o", "s"]
+[11.455424, "i", "a"]
+[11.455476, "o", "a"]
+[11.522638, "i", "f"]
+[11.522697, "o", "f"]
+[11.588859, "i", "e"]
+[11.588916, "o", "e"]
+[11.655246, "i", " "]
+[11.655302, "o", " "]
+[11.721647, "i", "m"]
+[11.721701, "o", "m"]
+[11.787780, "i", "o"]
+[11.787840, "o", "o"]
+[11.853935, "i", "d"]
+[11.853995, "o", "d"]
+[11.915354, "i", "e"]
+[11.915407, "o", "e"]
+[11.981377, "i", " "]
+[11.981433, "o", " "]
+[12.047459, "i", "s"]
+[12.047519, "o", "s"]
+[12.114324, "i", "e"]
+[12.114378, "o", "e"]
+[12.179634, "i", "t"]
+[12.179686, "o", "t"]
+[12.243328, "i", " "]
+[12.243374, "o", " "]
+[12.309349, "i", "e"]
+[12.309409, "o", "e"]
+[12.376090, "i", "n"]
+[12.376138, "o", "n"]
+[12.442520, "i", "a"]
+[12.442582, "o", "a"]
+[12.509806, "i", "b"]
+[12.509865, "o", "b"]
+[12.576685, "i", "l"]
+[12.576738, "o", "l"]
+[12.643696, "i", "e"]
+[12.643752, "o", "e"]
+[12.710067, "i", "d"]
+[12.710121, "o", "d"]
+[12.779593, "i", "\r"]
+[12.779666, "o", "\r\n"]
+[12.788138, "o", "enabled\t(file: /var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)\r\n"]
+[12.788755, "o", "$ "]
+[15.316550, "i", "exit\r"]
+[15.316645, "o", "e"]
+[15.316690, "o", "xit\r\n"]
+[15.316750, "o", "exit\r\n"]
diff --git a/docs/assets/casts/mode-toggle.svg b/docs/assets/casts/mode-toggle.svg
index 22dbb04..0e6d4d8 100644
--- a/docs/assets/casts/mode-toggle.svg
+++ b/docs/assets/casts/mode-toggle.svg
@@ -1 +1 @@
-Thedefaultinteractiveshellisnowzsh.Toupdateyouraccounttousezsh,pleaserun`chsh-s/bin/zsh`.Formoredetails,pleasevisithttps://support.apple.com/kb/HT208050.bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-do$$f$failsafemodegetenabled(default;nosourceresolved)$failsafemodesetdisableddisabled(file:/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)$failsafemodesetenabledenabled(file:/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)$exitbash-3.2$bash-3.2$ebash-3.2$exportPS1='$bash-3.2$exportPS1='$';cd"/var/foldebash-3.2$exportPS1='$';cd"/var/folders/bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48sybash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syckbash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsacs-home/cs-home/prcs-home/projcs-home/project";clear$failsafemodesete$eexit
\ No newline at end of file
+Thedefaultinteractiveshellisnowzsh.Toupdateyouraccounttousezsh,pleaserun`chsh-s/bin/zsh`.Formoredetails,pleasevisithttps://support.apple.com/kb/HT208050.bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/project";clear$$f$fa$fai$fail$fails$failsa$failsaf$failsafe$failsafem$failsafemo$failsafemod$failsafemode$failsafemodeg$failsafemodege$failsafemodegetenabled(default;nosourceresolved)$failsafemodes$failsafemodese$failsafemodeset$failsafemodesetdisableddisabled(file:/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)$failsafemodesetenabledenabled(file:/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.claude/pane-mode/w0t1p0:E03E0B4D-6630-49D4-96E2-ADCE77DB2402)$exitbash-3.2$bash-3.2$ebash-3.2$exportPS1='$';cbash-3.2$exportPS1='$';cd"/var/folders/12/bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__4bash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gbash-3.2$exportPS1='$';cd"/var/folders/12/mydd4m455zn9t__48syck4940000gncs-home$failsafemodesetd$failsafemodesetdi$failsafemodesetdis$failsafemodesetdisa$failsafemodesetdisab$failsafemodesetdisabl$failsafemodesetdisable$failsafemodesete$failsafemodeseten$failsafemodesetena$failsafemodesetenab$failsafemodesetenabl$failsafemodesetenable$eexit
\ No newline at end of file
diff --git a/docs/assets/casts/tmux-hotkey.cast b/docs/assets/casts/tmux-hotkey.cast
new file mode 100644
index 0000000..769179e
--- /dev/null
+++ b/docs/assets/casts/tmux-hotkey.cast
@@ -0,0 +1,177 @@
+{"version":2,"width":92,"height":22,"timestamp":1784339627}
+[0.003447, "o", "\u001b[?1034h"]
+[0.003498, "o", "bash-3.2$ "]
+[0.008330, "i", "export PS1='$ '; clear\r"]
+[0.008379, "o", "ex"]
+[0.008399, "o", "port PS1='$ '; clea"]
+[0.008404, "o", "r\r\n"]
+[0.010657, "o", "\u001b[3J\u001b[H\u001b[2J"]
+[0.010930, "o", "$ "]
+[0.519047, "i", "tmux -L failsafe-demo -f \"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.demo-tmux.conf\" new-session\r"]
+[0.519124, "o", "t"]
+[0.519140, "o", "mux -L failsafe-"]
+[0.519144, "o", "dem"]
+[0.519147, "o", "o "]
+[0.519149, "o", "-"]
+[0.519240, "o", "f \"/var/folders/12/mydd4m455zn9t__48syck494"]
+[0.519245, "o", "00"]
+[0.519248, "o", "0"]
+[0.519315, "o", "0gn/T//failsafe-docs"]
+[0.519332, "o", "- \rh"]
+[0.519377, "o", "ome/.demo-tmux.conf\" new-"]
+[0.519415, "o", "session\r\n"]
+[0.536508, "o", "\u001b[?1049h\u001b[?1h\u001b=\u001b[H\u001b[2J\u001b[?12l\u001b[?25h\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1005l\u001b[?2004h\u001b[?2031h\u001b[?996n\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[?1006l\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[1;1H\u001b[1;22r\u001b[c\u001b[\u003ec\u001b[\u003eq\u001b]10;?\u001b\\\u001b]11;?\u001b\\\u001b[1;1H"]
+[0.537440, "o", "\u001b[?25l\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\u001b[30m\u001b[42m\r\n[0] 0:zsh* | 22:53 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[1;1H\u001b[18t\u001b[14t\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[?1006l\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[1;1H\u001b[1;22r"]
+[0.537718, "o", "\u001b[?25l\u001b[1;1H\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\u001b[30m\u001b[42m\r\n[0] 0:zsh* | 22:53 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[1;1H"]
+[0.546837, "o", "$ "]
+[0.712085, "o", "\u001b[?25l\u001b[30m\u001b[42m\u001b[22;1H[0] 0:zsh* \u001b[32m🔒 on\u001b[30m | 22:53 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[1;3H"]
+[1.537387, "o", "\u001b[?25l\u001b[30m\u001b[42m\u001b[22;1H[0] 0:bash* \u001b[32m🔒 on\u001b[30m | 22:53 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[1;3H"]
+[2.048995, "i", "f"]
+[2.049281, "o", "f"]
+[2.113980, "i", "a"]
+[2.114158, "o", "a"]
+[2.179498, "i", "i"]
+[2.179702, "o", "i"]
+[2.248241, "i", "l"]
+[2.248421, "o", "l"]
+[2.309745, "i", "s"]
+[2.309924, "o", "s"]
+[2.376084, "i", "a"]
+[2.376238, "o", "a"]
+[2.441364, "i", "f"]
+[2.441571, "o", "f"]
+[2.504391, "i", "e"]
+[2.504547, "o", "e"]
+[2.567528, "i", " "]
+[2.567725, "o", " "]
+[2.633144, "i", "e"]
+[2.633297, "o", "e"]
+[2.702421, "i", "x"]
+[2.702679, "o", "x"]
+[2.777380, "i", "p"]
+[2.777674, "o", "p"]
+[2.845457, "i", "l"]
+[2.845641, "o", "l"]
+[2.908864, "i", "a"]
+[2.909128, "o", "a"]
+[2.974772, "i", "i"]
+[2.974940, "o", "i"]
+[3.043371, "i", "n"]
+[3.043560, "o", "n"]
+[3.105250, "i", " "]
+[3.105446, "o", " "]
+[3.168726, "i", "\""]
+[3.168997, "o", "\""]
+[3.230665, "i", "k"]
+[3.230873, "o", "k"]
+[3.291591, "i", "u"]
+[3.291803, "o", "u"]
+[3.355453, "i", "b"]
+[3.355659, "o", "b"]
+[3.419526, "i", "e"]
+[3.419743, "o", "e"]
+[3.485751, "i", "c"]
+[3.485976, "o", "c"]
+[3.552570, "i", "t"]
+[3.552810, "o", "t"]
+[3.619228, "i", "l"]
+[3.619436, "o", "l"]
+[3.687921, "i", " "]
+[3.688058, "o", " "]
+[3.750545, "i", "d"]
+[3.750720, "o", "d"]
+[3.816643, "i", "e"]
+[3.816775, "o", "e"]
+[3.878384, "i", "l"]
+[3.878587, "o", "l"]
+[3.944297, "i", "e"]
+[3.944530, "o", "e"]
+[4.007995, "i", "t"]
+[4.008145, "o", "t"]
+[4.070031, "i", "e"]
+[4.070186, "o", "e"]
+[4.135625, "i", " "]
+[4.135800, "o", " "]
+[4.201743, "i", "n"]
+[4.202204, "o", "n"]
+[4.273235, "i", "s"]
+[4.273450, "o", "s"]
+[4.337951, "i", " "]
+[4.338204, "o", " "]
+[4.410229, "i", "p"]
+[4.410337, "o", "p"]
+[4.475813, "i", "a"]
+[4.475929, "o", "a"]
+[4.544163, "i", "y"]
+[4.544428, "o", "y"]
+[4.609812, "i", "m"]
+[4.610036, "o", "m"]
+[4.676085, "i", "e"]
+[4.676330, "o", "e"]
+[4.744958, "i", "n"]
+[4.745251, "o", "n"]
+[4.814069, "i", "t"]
+[4.814248, "o", "t"]
+[4.882832, "i", "s"]
+[4.882992, "o", "s"]
+[4.957045, "i", "\""]
+[4.957237, "o", "\""]
+[5.025738, "i", "\r"]
+[5.026203, "o", "\r\n"]
+[5.039486, "o", "failsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego)\r\n"]
+[5.044504, "o", "── call 1: kubectl ──\r\nVerb: delete\r\nPositional: ns payments\r\nEffective cwd: /Users/tombar/Code/failsafe\r\nMode: enabled\r\nPolicy chain (7 modules at this cwd):\r\n [bundled] bundled/aws.rego\r\n [bundled] bundled/failsafe.rego\r\n [bundled] bundled/git.rego\r\n [bundled] bundled/helm.rego\r\n [bundled] bundled/kubectl.rego\r\n [bundled] bundled/terraform.rego\r\n [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted]\r\nDecision: BLOCK\r\nReason : kubectl delete blocked while failsafe is enabled\r\n"]
+[5.046735, "o", "$ "]
+[5.533418, "o", "\u001b[?7727h\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[?1006l\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[1;1H\u001b[1;22r\u001b[20;3H"]
+[5.534566, "o", "\u001b[?25l\u001b[H$ failsafe explain \"kubectl delete ns payments\"\u001b[K\r\nfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego)\u001b[K\r\n── call 1: kubectl ──\u001b[K\r\nVerb: delete\u001b[K\r\nPositional: ns payments\u001b[K\r\nEffective cwd: /Users/tombar/Code/failsafe\u001b[K\r\nMode: enabled\u001b[K\r\nPolicy chain (7 modules at this cwd):\u001b[K\r\n [bundled] bundled/aws.rego\u001b[K\r\n [bundled] bundled/failsafe.rego\u001b[K\r\n [bundled] bundled/git.rego\u001b[K\r\n [bundled] bundled/helm.rego\u001b[K\r\n [bundled] bundled/kubectl.rego\u001b[K\r\n [bundled] bundled/terraform.rego\u001b[K\r\n [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted]\u001b[K\r\nDecision: BLOCK\u001b[K\r\nReason : kubectl delete blocked while failsafe is enabled\u001b[K\r\n$ \u001b[K\r\n\u001b[K\u001b[30m\u001b[42m\r\n[0] 0:bash* \u001b[32m🔒 on\u001b[30m | 22:53 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[20;3H"]
+[7.065016, "i", "\u001b"]
+[7.065136, "i", "\u0014"]
+[7.078036, "o", "\u001b[?25l\u001b[H$ failsafe explain \"kubectl delete ns payments\" \u001b[2;1Hfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego) \u001b[5;1H── call 1: kubectl ── \u001b[6;1HVerb: delete \u001b[7;1HPositional: ns payments \u001b[8;1HEffective cwd: /Users/tombar\u001b[34C \u001b[9;1HMode: enabled \u001b[34C \u001b[10;1HPolicy chain (7 modules at t\u001b[34C \u001b[11;1H [bundled] bundled/aws.rego\u001b[34C \u001b[12;1H [bundled] bundled/failsafe\u001b[34C "]
+[7.078168, "o", " \u001b[13;1H [bundled] bundled/git.rego \u001b[14;1H [bundled] bundled/helm.rego \u001b[15;1H [bundled] bundled/kubectl.rego \u001b[16;1H [bundled] bundled/terraform.rego \u001b[17;1H [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted] \u001b[18;1HDecision: BLOCK \u001b[19;1HReason : kubectl delete blocked while failsafe is enabled \u001b[20;1H$ \u001b[21;1H \u001b[30m\u001b[42m\u001b[22;1H[0] 0:bash* \u001b[32m🔒 on\u001b[30m | 22:53 \u001b[3"]
+[7.078330, "o", "9m\u001b[49m\u001b[8;29H┌────────────────────────────────┐\u001b[9;29H│\u001b(B\u001b[m │\u001b[10;29H│\u001b(B\u001b[m │\u001b[11;29H│\u001b(B\u001b[m │\u001b[12;29H└────────────────────────────────┘\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[9;30H"]
+[7.079243, "o", "\u001b[?25l\u001b[8;29H┌────────────────────────────────┐\u001b[9;29H│\u001b(B\u001b[m │\u001b[10;29H│\u001b(B\u001b[m │\u001b[11;29H│\u001b(B\u001b[m │\u001b[12;29H└────────────────────────────────┘\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[9;30H"]
+[7.082431, "o", "\n \u001b[1m\u001b[7m ⌨ Ctrl + Alt + T \u001b[11;30H\u001b(B\u001b[m"]
+[7.542685, "o", "\u001b[?25l\u001b[8;29H┌────────────────────────────────┐\u001b[9;29H│\u001b(B\u001b[m │\u001b[10;29H│\u001b(B\u001b[m \u001b[1m\u001b[7m ⌨ Ctrl + Alt + T \u001b(B\u001b[m │\u001b[11;29H│\u001b(B\u001b[m │\u001b[12;29H└────────────────────────────────┘\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[11;30H"]
+[8.188871, "o", "\u001b[?25l\u001b[H$ failsafe explain \"kubectl delete ns payments\"\u001b[K\r\nfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego)\u001b[K\r\n── call 1: kubectl ──\u001b[K\r\nVerb: delete\u001b[K\r\nPositional: ns payments\u001b[K\r\nEffective cwd: /Users/tombar/Code/failsafe\u001b[K\r\nMode: enabled\u001b[K\r\nPolicy chain (7 modules at this cwd):\u001b[K\r\n [bundled] bundled/aws.rego\u001b[K\r\n [bundled] bundled/failsafe.rego\u001b[K\r\n [bundled] bundled/git.rego\u001b[K\r\n [bundled] bundled/helm.rego\u001b[K\r\n [bundled] bundled/kubectl.rego\u001b[K\r\n [bundled] bundled/terraform.rego\u001b[K\r\n [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted]\u001b[K\r\nDecision: BLOCK\u001b[K\r\nReason : kubectl delete blocked while failsafe is enabled\u001b[K\r\n$ \u001b[K\r\n\u001b[K\u001b[30m\u001b[42m\r\n[0] 0:bash* \u001b[32m🔒 on\u001b[30m | 22:53 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[20;3H"]
+[8.375121, "o", "\u001b[?25l\u001b[30m\u001b[43m\u001b[22;1H🔒 failsafe: enabled → disabled \u001b(B\u001b[m\u001b[20;3H"]
+[9.879162, "o", "\u001b[H$ failsafe explain \"kubectl delete ns payments\"\u001b[K\r\nfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego)\u001b[K\r\n── call 1: kubectl ──\u001b[K\r\nVerb: delete\u001b[K\r\nPositional: ns payments\u001b[K\r\nEffective cwd: /Users/tombar/Code/failsafe\u001b[K\r\nMode: enabled\u001b[K\r\nPolicy chain (7 modules at this cwd):\u001b[K\r\n [bundled] bundled/aws.rego\u001b[K\r\n [bundled] bundled/failsafe.rego\u001b[K\r\n [bundled] bundled/git.rego\u001b[K\r\n [bundled] bundled/helm.rego\u001b[K\r\n [bundled] bundled/kubectl.rego\u001b[K\r\n [bundled] bundled/terraform.rego\u001b[K\r\n [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted]\u001b[K\r\nDecision: BLOCK\u001b[K\r\nReason : kubectl delete blocked while failsafe is enabled\u001b[K\r\n$ \u001b[K\r\n\u001b[K\u001b[30m\u001b[42m\r\n[0] 0:bash* \u001b[32m🔒 on\u001b[30m | 22:53 \u001b(B\u001b[m\u001b[20;3H\u001b[?12l\u001b[?25h"]
+[9.891086, "o", "\u001b[?25l\u001b[30m\u001b[42m\u001b[22;1H[0] 0:bash* \u001b[33m\u001b[1m🔓 sudo\u001b(B\u001b[m\u001b[30m\u001b[42m | 22:53 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[20;3H"]
+[11.903438, "i", "\u001b[A"]
+[11.903758, "o", "failsafe explain \"kubectl delete ns payments\""]
+[11.911463, "i", "\r"]
+[11.911654, "o", "\r\n"]
+[11.920490, "o", "\u001b[1;21r\u001b[1;1H\u001b[3S\u001b[18dfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego)\u001b[K\r\n\u001b[K\u001b[1;22r\u001b[21;1H"]
+[11.920584, "o", "\u001b[?25l\u001b[?12l\u001b[?25h"]
+[11.924396, "o", "── call 1: kubectl ──\u001b[1;21r\u001b[1;1H\u001b[15S\u001b[6BVerb: delete\u001b[K\r\nPositional: ns payments\u001b[K\r\nEffective cwd: /Users/tombar/Code/failsafe\u001b[K\r\nMode: disabled\u001b[K\r\nPolicy chain (7 modules at this cwd):\u001b[K\r\n [bundled] bundled/aws.rego\u001b[K\r\n [bundled] bundled/failsafe.rego\u001b[K\r\n [bundled] bundled/git.rego\u001b[K\r\n [bundled] bundled/helm.rego\u001b[K\r\n [bundled] bundled/kubectl.rego\u001b[K\r\n [bundled] bundled/terraform.rego\u001b[K\r\n [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted]\u001b[K\r\nDecision: ALLOW\u001b[K\r\n\u001b[K\r\n\u001b[K\u001b[1;22r\u001b[21;1H"]
+[11.924528, "o", "\u001b[?25l\u001b[?12l\u001b[?25h"]
+[11.927761, "o", "$ "]
+[13.547983, "o", "\u001b[?25l\u001b[30m\u001b[42m\r\n[0] 0:bash* \u001b[33m\u001b[1m🔓 sudo\u001b(B\u001b[m\u001b[30m\u001b[42m | 22:54 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[21;3H"]
+[13.944952, "i", "\u001b"]
+[13.944991, "i", "\u0014"]
+[13.955643, "o", "\u001b[?25l\u001b[HReason : kubectl delete blocked while failsafe is enabled \u001b[2;1H$ failsafe explain \"kubectl delete ns payments\" \u001b[3;1Hfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego) \u001b[6;1H── call 1: kubectl ── \u001b[7;1HVerb: delete \u001b[8;1HPositional: ns payments \u001b[34C \u001b[9;1HEffective cwd: /Users/tombar\u001b[34C \u001b[10;1HMode: disabled \u001b[34C \u001b[11;1HPolicy chain (7 modules at t\u001b[34C \u001b[12;1H [bundled] bundled/aws.rego\u001b[34C "]
+[13.955828, "o", " \u001b[13;1H [bundled] bundled/failsafe.rego \u001b[14;1H [bundled] bundled/git.rego \u001b[15;1H [bundled] bundled/helm.rego \u001b[16;1H [bundled] bundled/kubectl.rego \u001b[17;1H [bundled] bundled/terraform.rego \u001b[18;1H [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted] \u001b[19;1HDecision: ALLOW \u001b[20;1H \u001b[21;1H$ \u001b[30m\u001b[42m\u001b[22;1H[0] 0:bash* \u001b[33m\u001b[1m🔓 sudo\u001b(B\u001b[m\u001b[30m\u001b["]
+[13.956060, "o", "42m | 22:54 \u001b[39m\u001b[49m\u001b[8;29H┌────────────────────────────────┐\u001b[9;29H│\u001b(B\u001b[m │\u001b[10;29H│\u001b(B\u001b[m │\u001b[11;29H│\u001b(B\u001b[m │\u001b[12;29H└────────────────────────────────┘\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[9;30H"]
+[13.958091, "o", "\u001b[?25l\u001b[8;29H┌────────────────────────────────┐\u001b[9;29H│\u001b(B\u001b[m │\u001b[10;29H│\u001b(B\u001b[m │\u001b[11;29H│\u001b(B\u001b[m │\u001b[12;29H└────────────────────────────────┘\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[9;30H"]
+[13.959445, "o", "\n \u001b[1m\u001b[7m ⌨ Ctrl + Alt + T \u001b[11;30H\u001b(B\u001b[m"]
+[14.548528, "o", "\u001b[?25l\u001b[8;29H┌────────────────────────────────┐\u001b[9;29H│\u001b(B\u001b[m │\u001b[10;29H│\u001b(B\u001b[m \u001b[1m\u001b[7m ⌨ Ctrl + Alt + T \u001b(B\u001b[m │\u001b[11;29H│\u001b(B\u001b[m │\u001b[12;29H└────────────────────────────────┘\u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[11;30H"]
+[15.067822, "o", "\u001b[?25l\u001b[HReason : kubectl delete blocked while failsafe is enabled\u001b[K\r\n$ failsafe explain \"kubectl delete ns payments\"\u001b[K\r\nfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego)\u001b[K\r\n── call 1: kubectl ──\u001b[K\r\nVerb: delete\u001b[K\r\nPositional: ns payments\u001b[K\r\nEffective cwd: /Users/tombar/Code/failsafe\u001b[K\r\nMode: disabled\u001b[K\r\nPolicy chain (7 modules at this cwd):\u001b[K\r\n [bundled] bundled/aws.rego\u001b[K\r\n [bundled] bundled/failsafe.rego\u001b[K\r\n [bundled] bundled/git.rego\u001b[K\r\n [bundled] bundled/helm.rego\u001b[K\r\n [bundled] bundled/kubectl.rego\u001b[K\r\n [bundled] bundled/terraform.rego\u001b[K\r\n [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted]\u001b[K\r\nDecision: ALLOW\u001b[K\r\n\u001b[K\r\n$ \u001b[K\u001b[30m\u001b[42m\r\n[0] 0:bash* \u001b[33m\u001b[1m🔓 sudo\u001b(B\u001b[m\u001b[30m\u001b[42m | 22:54 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[21;3H"]
+[15.080701, "o", "\u001b[?25l\u001b[30m\u001b[43m\r\n🔒 failsafe: disabled → enabled \u001b(B\u001b[m\u001b[21;3H"]
+[16.583823, "o", "\u001b[HReason : kubectl delete blocked while failsafe is enabled\u001b[K\r\n$ failsafe explain \"kubectl delete ns payments\"\u001b[K\r\nfailsafe: repo /Users/tombar/Code/failsafe ships allow_override rules but is untrusted; overrides ignored. Run `failsafe trust .` to enable. (file: /Users/tombar/Code/failsafe/.failsafe.rego)\u001b[K\r\n── call 1: kubectl ──\u001b[K\r\nVerb: delete\u001b[K\r\nPositional: ns payments\u001b[K\r\nEffective cwd: /Users/tombar/Code/failsafe\u001b[K\r\nMode: disabled\u001b[K\r\nPolicy chain (7 modules at this cwd):\u001b[K\r\n [bundled] bundled/aws.rego\u001b[K\r\n [bundled] bundled/failsafe.rego\u001b[K\r\n [bundled] bundled/git.rego\u001b[K\r\n [bundled] bundled/helm.rego\u001b[K\r\n [bundled] bundled/kubectl.rego\u001b[K\r\n [bundled] bundled/terraform.rego\u001b[K\r\n [repo] /Users/tombar/Code/failsafe/.failsafe.rego [untrusted]\u001b[K\r\nDecision: ALLOW\u001b[K\r\n\u001b[K\r\n$ \u001b[K\u001b[30m\u001b[42m\r\n[0] 0:bash* \u001b[33m\u001b[1m🔓 sudo\u001b(B\u001b[m\u001b[30m\u001b[42m | 22:54 \u001b(B\u001b[m\u001b[21;3H\u001b[?12l\u001b[?25h"]
+[16.604752, "o", "\u001b[?25l\u001b[30m\u001b[42m\r\n[0] 0:bash* \u001b[32m🔒 on\u001b[30m | 22:54 \u001b(B\u001b[m\u001b[?12l\u001b[?25h\u001b[21;3H"]
+[17.099820, "i", "e"]
+[17.100120, "o", "e"]
+[17.170501, "i", "x"]
+[17.170767, "o", "x"]
+[17.234270, "i", "i"]
+[17.234483, "o", "i"]
+[17.299494, "i", "t"]
+[17.299782, "o", "t"]
+[17.363114, "i", "\r"]
+[17.363265, "o", "\u001b[1;21r\u001b[21;1H\n\u001b[K\u001b[1;22r\u001b[21;1H\u001b[?25l\u001b[?12l\u001b[?25h"]
+[17.363386, "o", "\u001b[1;21r\u001b[21;1H\n\u001b[Aexit\r\n\u001b[K\u001b[1;22r\u001b[21;1H\u001b[?25l\u001b[?12l\u001b[?25h"]
+[17.364505, "o", "\u001b[1;22r"]
+[17.364515, "o", "\u001b(B\u001b[m\u001b[?1l\u001b\u003e\u001b[H\u001b[2J\u001b[?12l\u001b[?25h\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?1005l\u001b[?2004l\u001b[?7727l\u001b[?1004l\u001b[?1049l\u001b[?2031l"]
+[17.364571, "o", "[exited]\r\n"]
+[17.364940, "o", "$ "]
+[17.369995, "i", "exit\r"]
+[17.370059, "o", "e"]
+[17.370121, "o", "xit\r\nexit\r\n"]
diff --git a/docs/assets/casts/tmux-hotkey.svg b/docs/assets/casts/tmux-hotkey.svg
new file mode 100644
index 0000000..0db122c
--- /dev/null
+++ b/docs/assets/casts/tmux-hotkey.svg
@@ -0,0 +1 @@
+$$tmux-Lfailsafe-demo-f"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docs-home/.demo-tmux.conf"new-session[0] 0:zsh* | 22:53[0] 0:bash*🔒 on| 22:53$failsafe$failsafeexplain$failsafeexplain"kubectl$failsafeexplain"kubectldelete$failsafeexplain"kubectldeletens$failsafeexplain"kubectldeletenspayments"failsafe:repo/Users/tombar/Code/failsafeshipsallow_overriderulesbutisuntrusted;overridesignored.Run`failsafetrust.`toenable.(file:/Users/tombar/Code/failsafe/.failsafe.rego)──call1:kubectl──Verb:deletePositional:nspaymentsEffectivecwd:/Users/tombar/Code/failsafeMode:enabledPolicychain(7modulesatthiscwd):[bundled]bundled/aws.rego[bundled]bundled/failsafe.rego[bundled]bundled/git.rego[bundled]bundled/helm.rego[bundled]bundled/kubectl.rego[bundled]bundled/terraform.rego[repo]/Users/tombar/Code/failsafe/.failsafe.rego[untrusted]Decision:BLOCKReason:kubectldeleteblockedwhilefailsafeisenabledEffectivecwd:/Users/tombar┌────────────────────────────────┐Mode:enabled││Policychain(7modulesatt││[bundled]bundled/aws.rego││[bundled]bundled/failsafe└────────────────────────────────┘Policychain(7modulesatt│⌨ Ctrl + Alt + T│[0] 0:bash*🔓 sudo| 22:53Mode:disabledDecision:ALLOW[0] 0:bash*🔓 sudo| 22:54Positional:nspayments┌────────────────────────────────┐Effectivecwd:/Users/tombar││Mode:disabled││[bundled]bundled/aws.rego└────────────────────────────────┘Mode:disabled│⌨ Ctrl + Alt + T│[0] 0:bash*🔒 on| 22:54$e$exitexit[exited]bash-3.2$bash-3.2$exbash-3.2$exportPS1='$';cleabash-3.2$exportPS1='$';clear$t$tmux-Lfailsafe-$tmux-Lfailsafe-dem$tmux-Lfailsafe-demo$tmux-Lfailsafe-demo-$tmux-Lfailsafe-demo-f"/var/folders/12/mydd4m455zn9t__48syck494$tmux-Lfailsafe-demo-f"/var/folders/12/mydd4m455zn9t__48syck49400$tmux-Lfailsafe-demo-f"/var/folders/12/mydd4m455zn9t__48syck494000$tmux-Lfailsafe-demo-f"/var/folders/12/mydd4m455zn9t__48syck4940000gn/T//failsafe-docshhome/.demo-tmux.conf"new-[0] 0:zsh*🔒 on| 22:53$f$fa$fai$fail$fails$failsa$failsaf$failsafee$failsafeex$failsafeexp$failsafeexpl$failsafeexpla$failsafeexplai$failsafeexplain"$failsafeexplain"k$failsafeexplain"ku$failsafeexplain"kub$failsafeexplain"kube$failsafeexplain"kubec$failsafeexplain"kubect$failsafeexplain"kubectld$failsafeexplain"kubectlde$failsafeexplain"kubectldel$failsafeexplain"kubectldele$failsafeexplain"kubectldelet$failsafeexplain"kubectldeleten$failsafeexplain"kubectldeletensp$failsafeexplain"kubectldeletenspa$failsafeexplain"kubectldeletenspay$failsafeexplain"kubectldeletenspaym$failsafeexplain"kubectldeletenspayme$failsafeexplain"kubectldeletenspaymen$failsafeexplain"kubectldeletenspayment$failsafeexplain"kubectldeletenspayments🔒 failsafe: enabled → disabled🔒 failsafe: disabled → enabled$ex$exi
\ No newline at end of file
diff --git a/docs/demos/README.md b/docs/demos/README.md
index 4e67b43..0a685ed 100644
--- a/docs/demos/README.md
+++ b/docs/demos/README.md
@@ -35,12 +35,19 @@ each session to [asciicast v2](https://docs.asciinema.org/manual/asciicast/v2/):
```bash
scripts/record-docs.sh # record .cast files
scripts/record-docs.sh --render # also render .svg (svg-term) or .gif (agg)
+scripts/record-docs.sh --claude # planned: also record the inside-Claude demo (see below)
```
If `virtui` is not installed the script **skips cleanly** (exit 0) with an
install hint — it never fails a build just because the recorder is absent, the
same "missing tool ⇒ skip, never a vacuous pass" rule the harness uses.
+`COLS`/`ROWS` set the recorded terminal size, `STEP_PAUSE` sets the linger
+between commands (`STEP_PAUSE=0` = fast). `TYPE_DELAY` sets the per-keystroke
+typing speed baked into the cast itself — default `0.05` (seconds/keystroke),
+`TYPE_DELAY=0` types instantly. Unlike playback speed, `TYPE_DELAY` is baked in
+at record time and can't be recovered by re-rendering.
+
## Scenarios
| Cast | Command | Documents |
@@ -48,17 +55,51 @@ same "missing tool ⇒ skip, never a vacuous pass" rule the harness uses.
| `explain-block.cast` | `failsafe explain "kubectl … delete ns payments"` | tutorials/getting-started, README — the guard blocking a destructive command |
| `mode-toggle.cast` | `failsafe mode get / set disabled / set enabled` | reference/modes, getting-started step 4 |
| `audit.cast` | `failsafe audit` | how-to/repo-policy — the policy chain in force |
+| `tmux-hotkey.cast` | `failsafe explain` blocked, then `Ctrl+Alt+T` pressed live through tmux | toggle/tmux — the keyboard toggle flipping block → allow → re-arm |
Add a scenario by appending one `record_scenario` block in the script; keep the
command byte-identical to the doc snippet it illustrates.
+## Planned: inside-Claude demo (`--claude`)
+
+The `--claude` flag exists in the script, but the inside-Claude scenario
+itself is still being redesigned — no `.cast`/`.svg` for it exists yet, and
+nothing in `docs/` references one. When it ships:
+
+- **Uses your real `$HOME`.** Claude Code auth lives in the Keychain and
+ `~/.claude`, neither of which can be sandboxed the way `mode-toggle` and
+ `explain-block` are. `--claude` opts into that instead of forcing it on
+ everyone who runs the script.
+- **Runs with your own real global config, unmodified.** The recorded Claude
+ session is not stripped down or given special instructions to skip its own
+ review steps. If another guard — a project hook, a permission prompt, a
+ Claude-side safety check — fires before failsafe does, that's the honest,
+ layered result, and that's what gets recorded.
+- **Spends a few tokens per re-record.** It's a real Claude Code turn against
+ the real API, not a mock.
+- **Never runs in CI.** Only invoked on demand, by hand, when the docs need a
+ refresh.
+
+The recording ships once the scenario is reliable.
+
## What is **not** automated (honest split)
-virtui is headless/VT100, so it can only record the **CLI** surface. The
-GUI-only surfaces — WezTerm toasts & `format-tab-title`, iTerm2's Python-runtime
-keybinding — are not recordable here and remain **manual**, exactly the
-STATIC / LIVE-MANUAL surfaces called out in `test/docs/REPORT.md`. This script
-automates the CLI documentation flow, not the *full* flow.
+virtui is headless/VT100, so most GUI-only surfaces stay out of reach —
+WezTerm toasts & `format-tab-title`, iTerm2's Python-runtime keybinding are
+not recordable here and remain **manual**, exactly the STATIC / LIVE-MANUAL
+surfaces called out in `test/docs/REPORT.md`.
+
+The tmux hotkey is the exception: virtui drives a real PTY, so it can press
+`Ctrl+Alt+T` *through tmux's root key table* — the same path a human's
+keyboard takes, and something even the bats harness can't do (see the
+`send-keys` comment in `test/docs/tmux.bats`: injecting keys into the pane
+app bypasses the root key table, so `bind -n` bindings can only be checked by
+asserting the binding is registered, not by firing it). `tmux-hotkey.cast` is
+the first cast in this suite that shows a **live keybinding firing**, not
+just its downstream effect.
+
+This script automates the CLI and tmux documentation flow, not the full
+WezTerm/iTerm2 flow.
## Requirements
diff --git a/docs/reference/modes.md b/docs/reference/modes.md
index 3cfb776..68982e9 100644
--- a/docs/reference/modes.md
+++ b/docs/reference/modes.md
@@ -22,7 +22,7 @@ Flipping the current pane between the two values with `mode get` / `mode set`:

-*Recording regenerated by [`scripts/record-docs.sh`](https://github.com/UndermountainCC/failsafe/blob/main/scripts/record-docs.sh).*
+*Recording regenerated by [`scripts/record-docs.sh`](https://github.com/UndermountainCC/failsafe/blob/main/scripts/record-docs.sh).* Prefer a keyboard shortcut? See the [tmux toggle guide](../toggle/tmux.md) — the same flip bound to `Ctrl+Alt+T`, recorded live.
---
diff --git a/docs/toggle/tmux.md b/docs/toggle/tmux.md
index 3ce15f8..b201600 100644
--- a/docs/toggle/tmux.md
+++ b/docs/toggle/tmux.md
@@ -12,6 +12,10 @@ ambiguity. Missing file = `read` (the safe default).
Bind a key to flip the focused pane between `enabled` and `disabled`.
+
+
+*Recording regenerated by [`scripts/record-docs.sh`](https://github.com/UndermountainCC/failsafe/blob/main/scripts/record-docs.sh).* The whole flow below, recorded live — the demo builds its tmux config from this page's own snippets, so what you see is exactly what this doc installs.
+
## 1. The toggle helper
Save as `~/.config/failsafe/tmux-toggle.sh` and `chmod +x` it. Writing the file directly
diff --git a/scripts/record-docs.sh b/scripts/record-docs.sh
index a9af27c..7a3d64f 100755
--- a/scripts/record-docs.sh
+++ b/scripts/record-docs.sh
@@ -11,26 +11,41 @@
# produces the demo recordings that *show* them running, from the same commands,
# so the two never drift.
#
-# It is deliberately scoped to the HEADLESS CLI surface (explain / mode / audit /
-# report). The GUI terminal-integration surfaces (WezTerm toasts & tab-title,
-# iTerm2's Python keybinding) are NOT recordable headlessly and stay manual —
-# see docs/demos/README.md, same honest split as test/docs/REPORT.md.
+# It covers the HEADLESS CLI surface (explain / mode / audit / report) AND two
+# live, keystroke-driven demos:
+# - tmux-hotkey : the Ctrl+Alt+T per-pane toggle flipping enabled↔disabled
+# live, re-running the same command to BLOCK then ALLOW.
+# - claude-hook-block: failsafe blocking a kubectl delete *inside Claude Code*,
+# then the same hotkey flipping the guard without leaving
+# the Claude TUI. (opt-in: --claude; uses the REAL home so
+# Claude's Keychain/~/.claude auth works, spends a few
+# tokens — see record_claude_hook for the safety notes.)
+#
+# The tmux configs for the live demos are materialised FROM docs/toggle/tmux.md's
+# own fenced blocks (via test/docs/lib/extract.sh), so the recording can't drift
+# from the documented setup either.
#
# Requirements (all optional — missing tools downgrade gracefully, never fail):
# - virtui https://github.com/honeybadge-labs/virtui (the recorder)
# - agg https://github.com/asciinema/agg (.cast -> .gif) [optional render]
# - svg-term npm i -g svg-term-cli (.cast -> .svg) [optional render]
+# - tmux (the two live demos)
+# - claude Claude Code CLI (--claude demo only)
#
# Usage:
-# scripts/record-docs.sh # record all scenarios into docs/assets/casts/
-# scripts/record-docs.sh --render # also render .cast -> .svg/.gif if a renderer is present
-# COLS=100 ROWS=24 scripts/record-docs.sh
+# scripts/record-docs.sh # record headless + tmux-hotkey demos
+# scripts/record-docs.sh --render # also render .cast -> .svg/.gif
+# scripts/record-docs.sh --claude # additionally record the inside-Claude demo
+# scripts/record-docs.sh --claude --render # both, in any order
+# COLS=100 ROWS=24 TYPE_DELAY=0 scripts/record-docs.sh
set -euo pipefail
# ── Resolve paths ────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUT_DIR="${OUT_DIR:-$REPO_ROOT/docs/assets/casts}"
+EXTRACT="$REPO_ROOT/test/docs/lib/extract.sh"
+TMUX_DOC="$REPO_ROOT/docs/toggle/tmux.md"
COLS="${COLS:-92}"
ROWS="${ROWS:-22}"
# Pause (seconds) held AFTER each command's output, so playback lingers on the
@@ -38,8 +53,23 @@ ROWS="${ROWS:-22}"
# wall-clock time, so a host-side idle gap becomes a visible pause baked into the
# cast (re-rendering can't add it — it must be recorded). STEP_PAUSE=0 = fast.
STEP_PAUSE="${STEP_PAUSE:-2}"
+# Per-character delay (seconds) for the human-visible typing effect. Every command
+# is typed one glyph at a time via `virtui type` with this gap between glyphs, so
+# playback shows a person typing, not text teleporting in. TYPE_DELAY=0 = instant.
+# NOTE: this bakes real wall-clock timing into the cast, so casts (and their SVGs)
+# differ byte-for-byte on every re-record even when behaviour is identical.
+TYPE_DELAY="${TYPE_DELAY:-0.05}"
+
+# ── Arg parsing: --render and --claude, in any order ─────────────────────────
RENDER=0
-[[ "${1:-}" == "--render" ]] && RENDER=1
+CLAUDE=0
+for arg in "$@"; do
+ case "$arg" in
+ --render) RENDER=1 ;;
+ --claude) CLAUDE=1 ;;
+ *) printf 'record-docs.sh: unknown arg %q (ignored)\n' "$arg" >&2 ;;
+ esac
+done
mkdir -p "$OUT_DIR"
@@ -57,12 +87,17 @@ log "building failsafe -> $BINDIR/failsafe"
( cd "$REPO_ROOT" && go build -o "$BINDIR/failsafe" ./cmd/failsafe )
export PATH="$BINDIR:$PATH"
+# ── Capture the REAL home BEFORE sandboxing ──────────────────────────────────
+# The inside-Claude demo (record_claude_hook) needs it: Claude Code authenticates
+# against the macOS Keychain and ~/.claude, which a throwaway HOME would break.
+REAL_HOME="$HOME"
+
# ── Sandbox HOME so demos never touch the real ~/.config/failsafe ────────────
# failsafe resolves all of its state from $HOME (policy.rego, trusted-repos.yaml,
# decisions.jsonl, pane-mode files). A throwaway HOME keeps demos side-effect
# free, mirroring helpers.bash in the doc-validation harness. The path is FIXED
# (not mktemp-random) so the cwd printed by `explain` is readable and identical
-# on every re-record — committed SVGs then diff only when behaviour changes.
+# on every re-record.
SANDBOX_HOME="${TMPDIR:-/tmp}/failsafe-docs-home"
rm -rf "$SANDBOX_HOME"
export HOME="$SANDBOX_HOME"
@@ -73,6 +108,8 @@ unset FAILSAFE_MODE FAILSAFE_LOG 2>/dev/null || true
DAEMON_STARTED=0
cleanup() {
[[ "$DAEMON_STARTED" == 1 ]] && virtui daemon stop >/dev/null 2>&1 || true
+ tmux -L failsafe-demo kill-server >/dev/null 2>&1 || true
+ tmux -L failsafe-claude-demo kill-server >/dev/null 2>&1 || true
rm -rf "$BINDIR" "$SANDBOX_HOME"
}
trap cleanup EXIT
@@ -80,9 +117,24 @@ trap cleanup EXIT
log "starting virtui daemon"
virtui daemon start >/dev/null 2>&1 && DAEMON_STARTED=1 || warn "daemon may already be running"
+# ── type_line SID TEXT ───────────────────────────────────────────────────────
+# Type TEXT one character at a time (human-visible), then press Enter. This is
+# the whole reason the casts read like someone at a keyboard rather than a script
+# pasting. Single chars — including space, ", -, / — go through `virtui type`
+# verbatim (verified). TYPE_DELAY=0 makes it instant.
+type_line() {
+ local sid="$1" text="$2" i ch
+ for (( i = 0; i < ${#text}; i++ )); do
+ ch="${text:i:1}"
+ virtui type "$sid" "$ch" >/dev/null
+ [[ "$TYPE_DELAY" == 0 ]] || sleep "$TYPE_DELAY"
+ done
+ virtui press "$sid" Enter >/dev/null
+}
+
# ── record_scenario NAME -- "cmd|wait-for-text" ["cmd|wait" …] ───────────────
-# Spawns a recorded bash session, normalises the prompt, runs each step typing
-# the command and waiting for the given screen text, then exits to flush the cast.
+# Spawns a recorded bash session, normalises the prompt, types each command
+# char-by-char and waits for the given screen text, then exits to flush the cast.
record_scenario() {
local name="$1"; shift
[[ "$1" == "--" ]] && shift
@@ -96,17 +148,19 @@ record_scenario() {
| sed -n 's/^session_id: //p')"
[[ -n "$sid" ]] || { warn "could not start session for $name"; return 1; }
- # Quiet, deterministic prompt; start from the sandbox project dir.
+ # Quiet, deterministic prompt; start from the sandbox project dir. (Setup, not
+ # part of the visible story — kept as an instant exec, not char-by-char.)
virtui exec "$sid" "export PS1='\$ '; cd \"$HOME/project\"; clear" --wait-stable >/dev/null
local step cmd want
for step in "$@"; do
cmd="${step%%|*}"
want="${step#*|}"
+ type_line "$sid" "$cmd"
if [[ "$want" == "$step" || -z "$want" ]]; then
- virtui exec "$sid" "$cmd" --wait-stable >/dev/null
+ virtui wait "$sid" --stable >/dev/null
else
- virtui exec "$sid" "$cmd" --wait "$want" >/dev/null
+ virtui wait "$sid" --text "$want" >/dev/null
fi
sleep "$STEP_PAUSE" # linger on this command's output before the next one
done
@@ -115,6 +169,49 @@ record_scenario() {
virtui kill "$sid" >/dev/null 2>&1 || true # finalize/flush the recording
}
+# ── build_tmux_config CFGDIR CONF ────────────────────────────────────────────
+# Materialise the documented tmux integration into CFGDIR + CONF, straight from
+# docs/toggle/tmux.md's fenced blocks so the demo can't drift from the docs.
+# - toggle + status helper scripts -> CFGDIR/{tmux-toggle,tmux-status}.sh (+x)
+# - CONF = the doc's key-binding block + status block, with the doc's literal
+# ~/.config/failsafe paths rewritten to CFGDIR (so a temp CFGDIR works), plus
+# demo-only overrides:
+# status-interval 1 — snappier status flip on toggle (doc ships 2)
+# default-command — force a clean `$ ` bash prompt in the pane (macOS's
+# default login shell would otherwise pollute it)
+build_tmux_config() {
+ local cfgdir="$1" conf="$2"
+ mkdir -p "$cfgdir"
+ "$EXTRACT" "$TMUX_DOC" bash 1 "The toggle helper" > "$cfgdir/tmux-toggle.sh"
+ "$EXTRACT" "$TMUX_DOC" bash 1 "Status indicator" > "$cfgdir/tmux-status.sh"
+ chmod +x "$cfgdir/tmux-toggle.sh" "$cfgdir/tmux-status.sh"
+
+ {
+ "$EXTRACT" "$TMUX_DOC" tmux 1 "The key binding"
+ "$EXTRACT" "$TMUX_DOC" tmux 1 "Status indicator"
+ printf '\n# --- demo-only overrides ---\n'
+ printf 'set -g status-interval 1\n'
+ printf 'set -g display-time 1500\n'
+ # Keycast wrapper (demo polish only): the doc's C-M-t binding is materialised
+ # ABOVE, then this later binding for the SAME key wins. It pops up a big,
+ # centered "key was pressed" caption (tmux display-popup — YouTube-demo
+ # style, far more noticeable than a status-bar toast), then runs the doc's
+ # toggle script by its exact path — the toggle path is byte-identical to the
+ # doc; the wrapper only ADDS the on-screen key caption. The same real chord
+ # (press Escape Ctrl+T) still fires it through tmux's root key table, so the
+ # keystroke is genuine, not faked. The two %s args are reused as literal
+ # `\"` (escapes the popup's own -E command string) so bash/printf never
+ # re-interpret the \n / \033 escapes meant for the popup's inner printf.
+ printf "bind -n C-M-t run-shell \"tmux display-popup -w 34 -h 5 -E %sprintf '%s'; sleep 1.1%s; %s/tmux-toggle.sh '#{pane_id}'\"\n" \
+ '\"' "\n \033[1;7m ⌨ Ctrl + Alt + T \033[0m\n" '\"' "$cfgdir"
+ printf "set -g default-command \"env PS1='\$ ' BASH_SILENCE_DEPRECATION_WARNING=1 bash --norc\"\n"
+ } > "$conf"
+
+ # Rewrite the doc's ~/.config/failsafe reference to the real CFGDIR (absolute),
+ # so a throwaway CFGDIR resolves and there's no ~ / chain ambiguity.
+ sed -i '' "s#~/.config/failsafe#$cfgdir#g" "$conf"
+}
+
# ── Scenarios — the headless CLI demos ───────────────────────────────────────
# Each scenario maps to a doc page. Keep the commands identical to the snippets
# the doc-validation harness checks, so the recording and the prose can't drift.
@@ -136,6 +233,178 @@ record_scenario "mode-toggle" -- \
record_scenario "audit" -- \
"failsafe audit|block rule"
+# ── record_tmux_hotkey — the live per-pane toggle demo ───────────────────────
+# Ctrl+Alt+T flips the focused pane enabled↔disabled *live*: the same command
+# BLOCKs, then (after the chord) ALLOWs, then BLOCKs again — with the status bar
+# following along. Uses the sandbox HOME like the headless scenarios.
+record_tmux_hotkey() {
+ command -v tmux >/dev/null 2>&1 || { warn "tmux not found — skipping tmux-hotkey demo"; return 0; }
+ local name="tmux-hotkey" cast="$OUT_DIR/tmux-hotkey.cast"
+ local cfgdir="$HOME/.config/failsafe" conf="$HOME/.demo-tmux.conf"
+ log "recording $name -> $cast"
+
+ build_tmux_config "$cfgdir" "$conf"
+ tmux -L failsafe-demo kill-server >/dev/null 2>&1 || true # no zombie server
+
+ # -e: the macOS zsh-deprecation banner is printed by this OUTER bash (before
+ # tmux's default-command ever runs), so it must be silenced at spawn time.
+ local sid
+ sid="$(virtui run --record --record-path "$cast" --cols "$COLS" --rows "$ROWS" -e BASH_SILENCE_DEPRECATION_WARNING=1 bash \
+ | sed -n 's/^session_id: //p')"
+ [[ -n "$sid" ]] || { warn "could not start session for $name"; return 1; }
+
+ virtui exec "$sid" "export PS1='\$ '; clear" --wait-stable >/dev/null
+ # Launch tmux on a private socket with the materialised config (setup, exec'd).
+ virtui exec "$sid" "tmux -L failsafe-demo -f \"$conf\" new-session" --wait-stable >/dev/null
+ virtui wait "$sid" --stable >/dev/null
+
+ # BLOCK: guard is enabled by default.
+ type_line "$sid" 'failsafe explain "kubectl delete ns payments"'
+ virtui wait "$sid" --text "Decision: BLOCK" >/dev/null
+ sleep "$STEP_PAUSE"
+
+ # Chord → disabled. Status flips 🔒 on → 🔓 sudo. Wait on the ASCII "sudo"
+ # (the emoji cells render unpredictably; the word is stable).
+ virtui press "$sid" Escape Ctrl+T >/dev/null
+ virtui wait "$sid" --text "sudo" >/dev/null
+ sleep "$STEP_PAUSE"
+
+ # Re-run the SAME command from shell history (real Up + Enter). Now ALLOW.
+ virtui press "$sid" Up >/dev/null
+ virtui press "$sid" Enter >/dev/null
+ virtui wait "$sid" --text "Decision: ALLOW" >/dev/null
+ sleep "$STEP_PAUSE"
+
+ # Chord again → back to enabled. Status returns to 🔒 on; "sudo" disappears.
+ virtui press "$sid" Escape Ctrl+T >/dev/null
+ virtui wait "$sid" --gone "sudo" >/dev/null
+ sleep "$STEP_PAUSE"
+
+ type_line "$sid" "exit" # exit inner pane → tmux ends
+ virtui exec "$sid" "exit" --wait-stable >/dev/null 2>&1 || true # exit outer bash
+ virtui kill "$sid" >/dev/null 2>&1 || true
+
+ tmux -L failsafe-demo kill-server >/dev/null 2>&1 || true
+}
+
+record_tmux_hotkey
+
+# ── record_claude_hook — failsafe blocking a tool call INSIDE Claude Code ─────
+# Opt-in (--claude). This one is special:
+# * It runs with the REAL home (HOME=$REAL_HOME) because Claude Code authen-
+# ticates via the macOS Keychain and ~/.claude — a sandbox HOME breaks auth.
+# * It spends a few real tokens (one short turn that hits the PreToolUse hook).
+# * decisions.jsonl is kept clean via FAILSAFE_LOG -> a throwaway file, so the
+# user's real audit log doesn't grow.
+# * The tmux toggle helper still writes a pane-mode file under
+# $REAL_HOME/.claude/pane-mode/; we rm exactly that file on the way
+# out (diffing the dir before/after so we never touch the user's live panes).
+record_claude_hook() {
+ command -v claude >/dev/null 2>&1 || { warn "claude CLI not found — skipping inside-Claude demo"; return 0; }
+ command -v tmux >/dev/null 2>&1 || { warn "tmux not found — skipping inside-Claude demo"; return 0; }
+
+ warn "claude demo uses your REAL home ($REAL_HOME) for auth and spends a few tokens."
+ local name="claude-hook-block" cast="$OUT_DIR/claude-hook-block.cast"
+ local projdir logfile cfgdir conf
+ projdir="$(mktemp -d)"
+ logfile="$(mktemp)"
+ cfgdir="$(mktemp -d)" # NOT the real ~/.config/failsafe
+ conf="$cfgdir/demo-tmux.conf"
+ log "recording $name -> $cast"
+
+ build_tmux_config "$cfgdir" "$conf"
+ tmux -L failsafe-claude-demo kill-server >/dev/null 2>&1 || true
+
+ # The recorded Claude runs with the operator's own real global config, on
+ # purpose: this demo must never instruct the inner agent to skip its own
+ # review steps or confirmations. If other guards (skills, permission gates)
+ # fire before failsafe's hook, the recording shows layered defenses honestly —
+ # re-record on a machine with a plainer config if you want the hook-only shot.
+
+ # Snapshot the real pane-mode dir so we can remove only the file our toggle adds.
+ local panedir="$REAL_HOME/.claude/pane-mode"
+ local before after
+ before="$(mktemp)"; after="$(mktemp)"
+ mkdir -p "$panedir"
+ ( cd "$panedir" && ls -1 ) > "$before" 2>/dev/null || true
+
+ # -e: silence the macOS zsh-deprecation banner in the outer spawn shell (same
+ # reason as record_tmux_hotkey).
+ local sid
+ sid="$(virtui run --record --record-path "$cast" --cols "$COLS" --rows "$ROWS" -e BASH_SILENCE_DEPRECATION_WARNING=1 bash \
+ | sed -n 's/^session_id: //p')"
+ [[ -n "$sid" ]] || { warn "could not start session for $name"; return 1; }
+
+ # The interactive story runs in a subshell so a hung/timed-out wait (Claude is
+ # the flakiest surface here) degrades to a warning instead of aborting the whole
+ # script — the cast written so far is kept, and cleanup below always runs.
+ local rc=0
+ (
+ set -e
+ # Switch to the REAL home + isolated log + throwaway project (invisible setup).
+ virtui exec "$sid" "export HOME=\"$REAL_HOME\"; export FAILSAFE_LOG=\"$logfile\"; export PS1='\$ '; cd \"$projdir\"; clear" --wait-stable >/dev/null
+ # tmux with the temp-dir config on a private socket.
+ virtui exec "$sid" "tmux -L failsafe-claude-demo -f \"$conf\" new-session" --wait-stable >/dev/null
+ virtui wait "$sid" --stable >/dev/null
+
+ # Launch the real Claude Code CLI (real auth, user-global settings wire
+ # `failsafe hook` as PreToolUse).
+ type_line "$sid" "claude"
+ virtui wait "$sid" --stable >/dev/null
+ sleep 2
+ # First-run "Do you trust the files in this folder?" dialog — accept if present.
+ if virtui screenshot "$sid" | grep -qiE "trust"; then
+ virtui press "$sid" Enter >/dev/null
+ virtui wait "$sid" --stable >/dev/null
+ sleep 2
+ fi
+
+ # Ask Claude to do the blocked thing in natural language. The deterministic
+ # anchor is failsafe's reason ("… blocked while failsafe is enabled"), so we
+ # wait on "blocked". If the operator's own config intercepts first (a skill,
+ # a permission gate), the wait times out and this scenario degrades to a
+ # warning — that interception is legitimate and must not be worked around.
+ type_line "$sid" "delete the payments namespace with kubectl"
+ virtui wait "$sid" --text "blocked" --timeout 180000 >/dev/null
+ sleep "$STEP_PAUSE"
+
+ # The toggle-without-leaving-Claude shot: the chord is consumed by tmux's root
+ # key table (bind -n C-M-t) before Claude ever sees the bytes, so the guard
+ # flips and the status bar reads 🔓 sudo around the untouched Claude TUI.
+ virtui press "$sid" Escape Ctrl+T >/dev/null
+ virtui wait "$sid" --text "sudo" >/dev/null
+ sleep "$STEP_PAUSE"
+
+ type_line "$sid" "/exit" # leave Claude
+ virtui wait "$sid" --stable >/dev/null
+ ) || rc=$?
+ [[ "$rc" -eq 0 ]] || warn "inside-Claude interaction did not complete cleanly (rc=$rc) — cast may be partial; other scenarios are unaffected."
+
+ virtui exec "$sid" "exit" --wait-stable >/dev/null 2>&1 || true # leave tmux (pane→server ends)
+ virtui exec "$sid" "exit" --wait-stable >/dev/null 2>&1 || true # leave outer bash
+ virtui kill "$sid" >/dev/null 2>&1 || true
+ tmux -L failsafe-claude-demo kill-server >/dev/null 2>&1 || true
+
+ # Remove only the pane-mode file(s) our toggle created (never the user's live ones).
+ ( cd "$panedir" && ls -1 ) > "$after" 2>/dev/null || true
+ local newfile
+ while IFS= read -r newfile; do
+ if [[ -n "$newfile" ]]; then rm -f "$panedir/$newfile"; fi
+ done < <(comm -13 "$before" "$after")
+ rm -f "$before" "$after"
+ # Proof the isolation held: the block that just rendered went to the throwaway
+ # FAILSAFE_LOG, not the user's real decisions.jsonl.
+ if [[ -s "$logfile" ]]; then
+ log "inside-Claude demo: $(wc -l < "$logfile" | tr -d ' ') decision(s) captured in the isolated FAILSAFE_LOG (real decisions.jsonl untouched)."
+ fi
+ rm -rf "$projdir" "$cfgdir"
+ rm -f "$logfile"
+}
+
+if [[ "$CLAUDE" == 1 ]]; then
+ record_claude_hook || warn "inside-Claude demo failed; continuing with the other casts."
+fi
+
log "done — casts in $OUT_DIR"
ls -1 "$OUT_DIR"/*.cast 2>/dev/null >&2 || true