Repository: git-town/git-town

Issue:
make "detached" a proper config option
Right now the user can only enable detached mode on a one-off basis. Users in busy monerepos might want to enabled detached permanently and then pull in fresh changes once in a while by using `--detached=0`.

Discussion:
@stephenwade: Could we use `--no-detached` instead? `--detached=0` seems like a weird way to specify this.@kevgo: Thanks for the feedback! The CLI flag package we use doesn't really support this, but I was able to hack a working version together in https://github.com/git-town/git-town/pull/5458.@kevgo: This is now available as part of Git Town [21.5](https://github.com/git-town/git-town/releases/tag/v21.5.0).

Solution patch:
diff --git a/features/config/color.ui/always.feature b/features/config/color.ui/always.feature
index 9846ff37d..204d735b8 100644
--- a/features/config/color.ui/always.feature
+++ b/features/config/color.ui/always.feature
@@ -27,6 +27,7 @@ Feature: show the configuration
     And Git setting "git-town.ship-strategy" is "squash-merge"
     And Git setting "git-town.github-connector" is "api"
     And Git setting "git-town.gitlab-connector" is "api"
+    And Git setting "git-town.detached" is "true"
     When I run "git-town config"
     Then Git Town prints:
       """
@@ -69,6 +70,7 @@ Feature: show the configuration
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: yes
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
@@ -102,6 +104,7 @@ Feature: show the configuration
       strategy = "squash-merge"
 
       [sync]
+      detached = true
       feature-strategy = "rebase"
       perennial-strategy = "ff-only"
       prototype-strategy = "compress"
@@ -151,6 +154,7 @@ Feature: show the configuration
 
       Sync:
         auto-resolve phantom conflicts: no
+        run detached: yes
         run pre-push hook: yes
         feature sync strategy: rebase
         perennial sync strategy: ff-only
@@ -177,6 +181,7 @@ Feature: show the configuration
     And Git setting "git-town.sync-prototype-strategy" is "compress"
     And Git setting "git-town.unknown-branch-type" is "observed"
     And Git setting "git-town.auto-resolve" is "false"
+    And Git setting "git-town.detached" is "true"
     And the configuration file:
       """
       [branches]
@@ -200,6 +205,7 @@ Feature: show the configuration
       strategy = "api"
 
       [sync]
+      detached = false
       feature-strategy = "merge"
       perennial-strategy = "rebase"
       prototype-strategy = "rebase"
@@ -249,6 +255,7 @@ Feature: show the configuration
 
       Sync:
         auto-resolve phantom conflicts: no
+        run detached: yes
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: ff-only
@@ -309,6 +316,7 @@ Feature: show the configuration
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: no
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
@@ -375,6 +383,7 @@ Feature: show the configuration
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: no
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
diff --git a/features/config/setup/alias_override.feature b/features/config/setup/alias_override.feature
index 9d2632524..c9bd0742a 100644
--- a/features/config/setup/alias_override.feature
+++ b/features/config/setup/alias_override.feature
@@ -24,6 +24,7 @@ Feature: override an existing Git alias
       | sync prototype strategy     | enter      |
       | sync upstream               | enter      |
       | sync tags                   | enter      |
+      | detached                    | enter      |
       | share new branches          | enter      |
       | push hook                   | enter      |
       | ship strategy               | enter      |
diff --git a/features/config/setup/change_git_metadata.feature b/features/config/setup/change_git_metadata.feature
index db1d19a6f..47116c9a8 100644
--- a/features/config/setup/change_git_metadata.feature
+++ b/features/config/setup/change_git_metadata.feature
@@ -12,6 +12,7 @@ Feature: change existing information in Git metadata
     And local Git setting "git-town.share-new-branches" is "no"
     And local Git setting "git-town.push-hook" is "false"
     And local Git setting "git-town.sync-tags" is "false"
+    And local Git setting "git-town.detached" is "false"
     And local Git setting "git-town.ship-delete-tracking-branch" is "false"
     When I run "git-town config setup" and enter into the dialogs:
       | DIALOG                      | KEYS                   |
@@ -35,6 +36,7 @@ Feature: change existing information in Git metadata
       | sync prototype strategy     | down enter             |
       | sync upstream               | down enter             |
       | sync tags                   | down enter             |
+      | detached                    | up enter               |
       | share-new-branches          | down enter             |
       | push hook                   | down enter             |
       | ship strategy               | down down enter        |
@@ -60,6 +62,7 @@ Feature: change existing information in Git metadata
       | git config --global alias.ship "town ship"               |
       | git config --global alias.sync "town sync"               |
       | git config git-town.github-token gh-tok                  |
+      | git config git-town.detached true                        |
       | git config git-town.new-branch-type prototype            |
       | git config git-town.forge-type github                    |
       | git config git-town.github-connector api                 |
diff --git a/features/config/setup/codeberg_token.feature b/features/config/setup/codeberg_token.feature
index fe3240fdf..c83d6b573 100644
--- a/features/config/setup/codeberg_token.feature
+++ b/features/config/setup/codeberg_token.feature
@@ -27,6 +27,7 @@ Feature: enter the Codeberg API token
       | sync prototype strategy     | enter                  |                                             |
       | sync upstream               | enter                  |                                             |
       | sync tags                   | enter                  |                                             |
+      | detached                    | enter                  |                                             |
       | share new branches          | enter                  |                                             |
       | push hook                   | enter                  |                                             |
       | ship strategy               | enter                  |                                             |
@@ -35,6 +36,7 @@ Feature: enter the Codeberg API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.codeberg-token code-tok          |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.unknown-branch-type feature      |
       | git config git-town.push-hook true                   |
@@ -71,6 +73,7 @@ Feature: enter the Codeberg API token
       | sync prototype strategy     | enter                  |                                             |
       | sync upstream               | enter                  |                                             |
       | sync tags                   | enter                  |                                             |
+      | detached                    | enter                  |                                             |
       | share new branches          | enter                  |                                             |
       | push hook                   | enter                  |                                             |
       | ship strategy               | enter                  |                                             |
@@ -79,6 +82,7 @@ Feature: enter the Codeberg API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.codeberg-token code-tok          |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.forge-type codeberg              |
       | git config git-town.unknown-branch-type feature      |
@@ -117,6 +121,7 @@ Feature: enter the Codeberg API token
       | sync prototype strategy     | enter                  |                                             |
       | sync upstream               | enter                  |                                             |
       | sync tags                   | enter                  |                                             |
+      | detached                    | enter                  |                                             |
       | share new branches          | enter                  |                                             |
       | push hook                   | enter                  |                                             |
       | ship strategy               | enter                  |                                             |
@@ -125,6 +130,7 @@ Feature: enter the Codeberg API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.codeberg-token code-tok |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.unknown-branch-type feature      |
       | git config git-town.push-hook true                   |
@@ -162,6 +168,7 @@ Feature: enter the Codeberg API token
       | sync prototype strategy     | enter                                     |                                             |
       | sync upstream               | enter                                     |                                             |
       | sync tags                   | enter                                     |                                             |
+      | detached                    | enter                                     |                                             |
       | share new branches          | enter                                     |                                             |
       | push hook                   | enter                                     |                                             |
       | ship strategy               | enter                                     |                                             |
@@ -170,6 +177,7 @@ Feature: enter the Codeberg API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.codeberg-token code456  |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.unknown-branch-type feature      |
       | git config git-town.push-hook true                   |
diff --git a/features/config/setup/default_values.feature b/features/config/setup/default_values.feature
index d480df997..a7d6ba70c 100644
--- a/features/config/setup/default_values.feature
+++ b/features/config/setup/default_values.feature
@@ -28,6 +28,7 @@ Feature: Accepting all default values leads to a working setup
       | sync prototype strategy     | enter      |
       | sync upstream               | enter      |
       | sync tags                   | enter      |
+      | detached                    | enter      |
       | share new branches          | enter      |
       | push hook                   | enter      |
       | ship strategy               | enter      |
diff --git a/features/config/setup/fresh_default_values.feature b/features/config/setup/fresh_default_values.feature
index 394c2533c..69ed8def2 100644
--- a/features/config/setup/fresh_default_values.feature
+++ b/features/config/setup/fresh_default_values.feature
@@ -22,6 +22,7 @@ Feature: Accepting all default values in a brand-new Git repo leads to a working
       | sync prototype strategy     | enter      |
       | sync upstream               | enter      |
       | sync tags                   | enter      |
+      | detached                    | enter      |
       | share new branches          | enter      |
       | push hook                   | enter      |
       | ship strategy               | enter      |
diff --git a/features/config/setup/gitea_token.feature b/features/config/setup/gitea_token.feature
index a9b80909d..5252091b3 100644
--- a/features/config/setup/gitea_token.feature
+++ b/features/config/setup/gitea_token.feature
@@ -27,6 +27,7 @@ Feature: enter the Gitea API token
       | sync prototype strategy     | enter                   |                                             |
       | sync upstream               | enter                   |                                             |
       | sync tags                   | enter                   |                                             |
+      | detached                    | enter                   |                                             |
       | share new branches          | enter                   |                                             |
       | push hook                   | enter                   |                                             |
       | ship strategy               | enter                   |                                             |
@@ -35,6 +36,7 @@ Feature: enter the Gitea API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.gitea-token gitea-tok            |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.unknown-branch-type feature      |
       | git config git-town.push-hook true                   |
@@ -71,6 +73,7 @@ Feature: enter the Gitea API token
       | sync prototype strategy     | enter                     |                                             |
       | sync upstream               | enter                     |                                             |
       | sync tags                   | enter                     |                                             |
+      | detached                    | enter                     |                                             |
       | share new branches          | enter                     |                                             |
       | push hook                   | enter                     |                                             |
       | ship strategy               | enter                     |                                             |
@@ -79,6 +82,7 @@ Feature: enter the Gitea API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.gitea-token gitea-tok            |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.forge-type gitea                 |
       | git config git-town.unknown-branch-type feature      |
@@ -117,6 +121,7 @@ Feature: enter the Gitea API token
       | sync prototype strategy     | enter                   |                                             |
       | sync upstream               | enter                   |                                             |
       | sync tags                   | enter                   |                                             |
+      | detached                    | enter                   |                                             |
       | share new branches          | enter                   |                                             |
       | push hook                   | enter                   |                                             |
       | ship strategy               | enter                   |                                             |
@@ -125,6 +130,7 @@ Feature: enter the Gitea API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.gitea-token gitea-tok   |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.unknown-branch-type feature      |
       | git config git-town.push-hook true                   |
@@ -162,6 +168,7 @@ Feature: enter the Gitea API token
       | sync prototype strategy     | enter                                     |                                             |
       | sync upstream               | enter                                     |                                             |
       | sync tags                   | enter                                     |                                             |
+      | detached                    | enter                                     |                                             |
       | share new branches          | enter                                     |                                             |
       | push hook                   | enter                                     |                                             |
       | ship strategy               | enter                                     |                                             |
@@ -170,6 +177,7 @@ Feature: enter the Gitea API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.gitea-token 456         |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.unknown-branch-type feature      |
       | git config git-town.push-hook true                   |
diff --git a/features/config/setup/github_token.feature b/features/config/setup/github_token.feature
index 28b985fcc..ade47a2a2 100644
--- a/features/config/setup/github_token.feature
+++ b/features/config/setup/github_token.feature
@@ -28,6 +28,7 @@ Feature: enter the GitHub API token
       | sync prototype strategy     | enter             |                                             |
       | sync upstream               | enter             |                                             |
       | sync tags                   | enter             |                                             |
+      | detached                    | enter             |                                             |
       | share new branches          | enter             |                                             |
       | push hook                   | enter             |                                             |
       | ship strategy               | enter             |                                             |
@@ -36,6 +37,7 @@ Feature: enter the GitHub API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.github-token gh-tok              |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.github-connector api             |
       | git config git-town.unknown-branch-type feature      |
@@ -74,6 +76,7 @@ Feature: enter the GitHub API token
       | sync prototype strategy     | enter                          |                                             |
       | sync upstream               | enter                          |                                             |
       | sync tags                   | enter                          |                                             |
+      | detached                    | enter                          |                                             |
       | share new branches          | enter                          |                                             |
       | push hook                   | enter                          |                                             |
       | ship strategy               | enter                          |                                             |
@@ -82,6 +85,7 @@ Feature: enter the GitHub API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.github-token gh-tok              |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.forge-type github                |
       | git config git-town.github-connector api             |
@@ -122,6 +126,7 @@ Feature: enter the GitHub API token
       | sync prototype strategy     | enter                               |                                             |
       | sync upstream               | enter                               |                                             |
       | sync tags                   | enter                               |                                             |
+      | detached                    | enter                               |                                             |
       | share new branches          | enter                               |                                             |
       | push hook                   | enter                               |                                             |
       | ship strategy               | enter                               |                                             |
@@ -130,6 +135,7 @@ Feature: enter the GitHub API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --unset git-town.github-token             |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.github-connector api             |
       | git config git-town.unknown-branch-type feature      |
@@ -169,6 +175,7 @@ Feature: enter the GitHub API token
       | sync prototype strategy     | enter           |                                             |
       | sync upstream               | enter           |                                             |
       | sync tags                   | enter           |                                             |
+      | detached                    | enter           |                                             |
       | share new branches          | enter           |                                             |
       | push hook                   | enter           |                                             |
       | ship strategy               | enter           |                                             |
@@ -177,6 +184,7 @@ Feature: enter the GitHub API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.github-token ghtok      |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.github-connector api             |
       | git config git-town.unknown-branch-type feature      |
@@ -216,6 +224,7 @@ Feature: enter the GitHub API token
       | sync prototype strategy     | enter                                     |                                             |
       | sync upstream               | enter                                     |                                             |
       | sync tags                   | enter                                     |                                             |
+      | detached                    | enter                                     |                                             |
       | share new branches          | enter                                     |                                             |
       | push hook                   | enter                                     |                                             |
       | ship strategy               | enter                                     |                                             |
@@ -224,6 +233,7 @@ Feature: enter the GitHub API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.github-token 456        |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.github-connector api             |
       | git config git-town.unknown-branch-type feature      |
diff --git a/features/config/setup/gitlab_token.feature b/features/config/setup/gitlab_token.feature
index c23fbfd86..d9ffe1ebf 100644
--- a/features/config/setup/gitlab_token.feature
+++ b/features/config/setup/gitlab_token.feature
@@ -28,6 +28,7 @@ Feature: enter the GitLab API token
       | sync prototype strategy     | enter             |                                             |
       | sync upstream               | enter             |                                             |
       | sync tags                   | enter             |                                             |
+      | detached                    | enter             |                                             |
       | share new branches          | enter             |                                             |
       | push hook                   | enter             |                                             |
       | ship strategy               | enter             |                                             |
@@ -36,6 +37,7 @@ Feature: enter the GitLab API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.gitlab-token gl-tok              |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.gitlab-connector api             |
       | git config git-town.unknown-branch-type feature      |
@@ -73,6 +75,7 @@ Feature: enter the GitLab API token
       | sync prototype strategy     | enter             |                                             |
       | sync upstream               | enter             |                                             |
       | sync tags                   | enter             |                                             |
+      | detached                    | enter             |                                             |
       | share new branches          | enter             |                                             |
       | push hook                   | enter             |                                             |
       | ship strategy               | enter             |                                             |
@@ -81,6 +84,7 @@ Feature: enter the GitLab API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.gitlab-token gl-tok              |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.forge-type gitlab                |
       | git config git-town.gitlab-connector api             |
@@ -121,6 +125,7 @@ Feature: enter the GitLab API token
       | sync prototype strategy     | enter           |                                             |
       | sync upstream               | enter           |                                             |
       | sync tags                   | enter           |                                             |
+      | detached                    | enter           |                                             |
       | share new branches          | enter           |                                             |
       | push hook                   | enter           |                                             |
       | ship strategy               | enter           |                                             |
@@ -129,6 +134,7 @@ Feature: enter the GitLab API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.gitlab-token gltok      |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.gitlab-connector api             |
       | git config git-town.unknown-branch-type feature      |
@@ -168,6 +174,7 @@ Feature: enter the GitLab API token
       | sync prototype strategy     | enter                                     |                                             |
       | sync upstream               | enter                                     |                                             |
       | sync tags                   | enter                                     |                                             |
+      | detached                    | enter                                     |                                             |
       | share new branches          | enter                                     |                                             |
       | push hook                   | enter                                     |                                             |
       | ship strategy               | enter                                     |                                             |
@@ -176,6 +183,7 @@ Feature: enter the GitLab API token
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config --global git-town.gitlab-token 456        |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.gitlab-connector api             |
       | git config git-town.unknown-branch-type feature      |
diff --git a/features/config/setup/global_token_other_forge.feature b/features/config/setup/global_token_other_forge.feature
index a66f43d06..0ee9eb6f7 100644
--- a/features/config/setup/global_token_other_forge.feature
+++ b/features/config/setup/global_token_other_forge.feature
@@ -27,6 +27,7 @@ Feature: a global API token of another forge exists
       | sync prototype strategy     | enter           |                                             |
       | sync upstream               | enter           |                                             |
       | sync tags                   | enter           |                                             |
+      | detached                    | enter           |                                             |
       | share new branches          | enter           |                                             |
       | push hook                   | enter           |                                             |
       | ship strategy               | enter           |                                             |
@@ -35,6 +36,7 @@ Feature: a global API token of another forge exists
     Then Git Town runs the commands
       | COMMAND                                              |
       | git config git-town.github-token ghtok               |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config git-town.github-connector api             |
       | git config git-town.unknown-branch-type feature      |
diff --git a/features/config/setup/migrate_git_to_file.feature b/features/config/setup/migrate_git_to_file.feature
index 0b5b4ee9e..862697887 100644
--- a/features/config/setup/migrate_git_to_file.feature
+++ b/features/config/setup/migrate_git_to_file.feature
@@ -40,6 +40,7 @@ Feature: migrate existing configuration in Git metadata to a config file
       | sync prototype strategy     | enter      |
       | sync upstream               | enter      |
       | sync tags                   | enter      |
+      | detached                    | enter      |
       | share new branches          | enter      |
       | push hook                   | enter      |
       | ship strategy               | enter      |
diff --git a/features/config/setup/multiple_remotes.feature b/features/config/setup/multiple_remotes.feature
index ab5121a62..e43e40070 100644
--- a/features/config/setup/multiple_remotes.feature
+++ b/features/config/setup/multiple_remotes.feature
@@ -24,6 +24,7 @@ Feature: Configure a different development remote
       | sync prototype strategy     | enter      |
       | sync upstream               | enter      |
       | sync tags                   | enter      |
+      | detached                    | enter      |
       | share new branches          | enter      |
       | push hook                   | enter      |
       | ship strategy               | enter      |
diff --git a/features/config/setup/remove_git_metadata.feature b/features/config/setup/remove_git_metadata.feature
index 4df45cb52..6a4aec9e3 100644
--- a/features/config/setup/remove_git_metadata.feature
+++ b/features/config/setup/remove_git_metadata.feature
@@ -24,6 +24,7 @@ Feature: remove existing configuration in Git metadata
     And global Git setting "alias.set-parent" is "town set-parent"
     And global Git setting "alias.ship" is "town ship"
     And global Git setting "alias.sync" is "town sync"
+    And local Git setting "git-town.detached" is "true"
     And local Git setting "git-town.forge-type" is "github"
     And local Git setting "git-town.perennial-branches" is "qa"
     And local Git setting "git-town.perennial-regex" is "qa.*"
@@ -63,6 +64,7 @@ Feature: remove existing configuration in Git metadata
       | sync prototype strategy     | up enter                                                                    |                     |
       | sync upstream               | down enter                                                                  |                     |
       | sync tags                   | down enter                                                                  |                     |
+      | detached                    | down enter                                                                  |                     |
       | share new branches          | up enter                                                                    | enable              |
       | push hook                   | down enter                                                                  | enable              |
       | ship strategy               | down enter                                                                  |                     |
@@ -87,6 +89,7 @@ Feature: remove existing configuration in Git metadata
       | git config --global --unset alias.set-parent         |
       | git config --global --unset alias.ship               |
       | git config --global --unset alias.sync               |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config --unset git-town.forge-type               |
       | git config --unset git-town.hosting-origin-hostname  |
diff --git a/features/config/setup/remove_hosting_override.feature b/features/config/setup/remove_hosting_override.feature
index 02f4fd4a2..a8c9e3ced 100644
--- a/features/config/setup/remove_hosting_override.feature
+++ b/features/config/setup/remove_hosting_override.feature
@@ -23,6 +23,7 @@ Feature: remove an existing forge type override
       | sync prototype strategy     | enter                |                                             |
       | sync upstream               | enter                |                                             |
       | sync tags                   | enter                |                                             |
+      | detached                    | enter                |                                             |
       | share new branches          | enter                |                                             |
       | push hook                   | enter                |                                             |
       | ship strategy               | enter                |                                             |
@@ -32,6 +33,7 @@ Feature: remove an existing forge type override
   Scenario: result
     Then Git Town runs the commands
       | COMMAND                                              |
+      | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config --unset git-town.forge-type               |
       | git config git-town.unknown-branch-type feature      |
diff --git a/features/config/setup/skip_perennials.feature b/features/config/setup/skip_perennials.feature
index fb8bd4721..8d1ba25b8 100644
--- a/features/config/setup/skip_perennials.feature
+++ b/features/config/setup/skip_perennials.feature
@@ -23,6 +23,7 @@ Feature: don't ask for perennial branches if no branches that could be perennial
       | sync prototype strategy     | enter      |                                             |
       | sync upstream               | enter      |                                             |
       | sync tags                   | enter      |                                             |
+      | detached                    | enter      |                                             |
       | share new branches          | enter      |                                             |
       | push hook                   | enter      |                                             |
       | ship strategy               | enter      |                                             |
diff --git a/features/config/setup/unconfigured_with_complete_config_file.feature b/features/config/setup/unconfigured_with_complete_config_file.feature
index 0e3bc5585..efbfb6e11 100644
--- a/features/config/setup/unconfigured_with_complete_config_file.feature
+++ b/features/config/setup/unconfigured_with_complete_config_file.feature
@@ -29,6 +29,7 @@ Feature: don't ask for information already provided by the config file
       strategy = "api"
 
       [sync]
+      detached = false
       feature-strategy = "merge"
       perennial-strategy = "rebase"
       push-hook = true
diff --git a/features/config/setup/unconfigured_with_config_file_and_global_api_token.feature b/features/config/setup/unconfigured_with_config_file_and_global_api_token.feature
index 03e6b30ea..5e904a6af 100644
--- a/features/config/setup/unconfigured_with_config_file_and_global_api_token.feature
+++ b/features/config/setup/unconfigured_with_config_file_and_global_api_token.feature
@@ -31,6 +31,7 @@ Feature: don't ask for information already provided by the config file
       strategy = "api"
 
       [sync]
+      detached = false
       feature-strategy = "merge"
       perennial-strategy = "rebase"
       push-hook = true
diff --git a/features/config/setup/unconfigured_with_global_settings.feature b/features/config/setup/unconfigured_with_global_settings.feature
index a4bf61f37..9848b6373 100644
--- a/features/config/setup/unconfigured_with_global_settings.feature
+++ b/features/config/setup/unconfigured_with_global_settings.feature
@@ -25,6 +25,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
     And global Git setting "git-town.sync-prototype-strategy" is "compress"
     And global Git setting "git-town.sync-tags" is "false"
     And global Git setting "git-town.sync-upstream" is "false"
+    And global Git setting "git-town.detached" is "false"
     And global Git setting "git-town.unknown-branch-type" is "observed"
     When I run "git-town config setup" and enter into the dialogs:
       | DIALOG                      | KEYS        |
@@ -45,6 +46,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
       | sync prototype strategy     | enter       |
       | sync upstream               | enter       |
       | sync tags                   | enter       |
+      | detached                    | enter       |
       | share new branches          | enter       |
       | push hook                   | enter       |
       | ship strategy               | enter       |
diff --git a/features/config/setup/unconfigured_with_partial_config_file.feature b/features/config/setup/unconfigured_with_partial_config_file.feature
index fe4d472a3..90cb0a28e 100644
--- a/features/config/setup/unconfigured_with_partial_config_file.feature
+++ b/features/config/setup/unconfigured_with_partial_config_file.feature
@@ -42,6 +42,7 @@ Feature: ask for information not provided by the config file
       | sync feature strategy   | enter                 |
       | sync perennial strategy | enter                 |
       | sync prototype strategy | enter                 |
+      | detached                | enter                 |
       | share new branches      | enter                 |
       | push hook               | enter                 |
       | ship strategy           | enter                 |
@@ -49,6 +50,7 @@ Feature: ask for information not provided by the config file
     Then Git Town runs the commands
       | COMMAND                                             |
       | git config git-town.github-token gh-token           |
+      | git config git-town.detached false                  |
       | git config git-town.new-branch-type feature         |
       | git config git-town.github-connector api            |
       | git config git-town.perennial-branches branch-1     |
diff --git a/features/config/view/alternative_config_file.feature b/features/config/view/alternative_config_file.feature
index 039e68780..6fa42f841 100644
--- a/features/config/view/alternative_config_file.feature
+++ b/features/config/view/alternative_config_file.feature
@@ -2,6 +2,7 @@ Feature: show the configuration when using an alternative config file
 
   Scenario: all configured in config file with alternative filename
     Given a Git repo with origin
+    # TODO: change the filename to ".git-branches.toml"
     And file ".git-town.toml" with content
       """
       [branches]
@@ -50,6 +51,7 @@ Feature: show the configuration when using an alternative config file
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: no
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
diff --git a/features/config/view/config_file.feature b/features/config/view/config_file.feature
index 558b77100..e09878e11 100644
--- a/features/config/view/config_file.feature
+++ b/features/config/view/config_file.feature
@@ -41,6 +41,7 @@ Feature: display information from config file
 
       [sync]
       auto-resolve = false
+      detached = true
       feature-strategy = "rebase"
       perennial-strategy = "ff-only"
       prototype-strategy = "compress"
@@ -89,6 +90,7 @@ Feature: display information from config file
 
       Sync:
         auto-resolve phantom conflicts: no
+        run detached: yes
         run pre-push hook: yes
         feature sync strategy: rebase
         perennial sync strategy: ff-only
diff --git a/features/config/view/environment_variables.feature b/features/config/view/environment_variables.feature
index c0e3fba05..89503edca 100644
--- a/features/config/view/environment_variables.feature
+++ b/features/config/view/environment_variables.feature
@@ -23,6 +23,7 @@ Feature: display configuration defined in environment variables
       | GIT_TOWN_BITBUCKET_USERNAME          | bitbucket-user     |
       | GIT_TOWN_CODEBERG_TOKEN              | codeberg-token     |
       | GIT_TOWN_CONTRIBUTION_REGEX          | ^renovate/         |
+      | GIT_TOWN_DETACHED                    | true               |
       | GIT_TOWN_DEV_REMOTE                  | my-fork            |
       | GIT_TOWN_FEATURE_REGEX               | ^user-.*$          |
       | GIT_TOWN_FORGE_TYPE                  | gitlab             |
@@ -89,6 +90,7 @@ Feature: display configuration defined in environment variables
 
       Sync:
         auto-resolve phantom conflicts: no
+        run detached: yes
         run pre-push hook: no
         feature sync strategy: rebase
         perennial sync strategy: merge
diff --git a/features/config/view/git_metadata.feature b/features/config/view/git_metadata.feature
index a1e01eb50..5f5ad2798 100644
--- a/features/config/view/git_metadata.feature
+++ b/features/config/view/git_metadata.feature
@@ -24,6 +24,7 @@ Feature: display configuration from Git metadata
     And Git setting "git-town.ship-strategy" is "squash-merge"
     And Git setting "git-town.unknown-branch-type" is "observed"
     And Git setting "git-town.auto-resolve" is "false"
+    And Git setting "git-town.detached" is "true"
     When I run "git-town config"
     Then Git Town prints:
       """
@@ -66,6 +67,7 @@ Feature: display configuration from Git metadata
 
       Sync:
         auto-resolve phantom conflicts: no
+        run detached: yes
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
@@ -125,6 +127,7 @@ Feature: display configuration from Git metadata
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: no
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
diff --git a/features/config/view/in_subfolder.feature b/features/config/view/in_subfolder.feature
index 7cd2d9dc4..33e761096 100644
--- a/features/config/view/in_subfolder.feature
+++ b/features/config/view/in_subfolder.feature
@@ -51,6 +51,7 @@ Feature: show the configuration from a subfolder
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: no
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
diff --git a/features/config/view/lang/es.feature b/features/config/view/lang/es.feature
index b059dbdb5..fbcdc6f16 100644
--- a/features/config/view/lang/es.feature
+++ b/features/config/view/lang/es.feature
@@ -66,6 +66,7 @@ Feature: show the configuration in Spanish
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: no
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
diff --git a/features/config/view/multiple_config_sources.feature b/features/config/view/multiple_config_sources.feature
index 0a9ea3f5d..02fb8290f 100644
--- a/features/config/view/multiple_config_sources.feature
+++ b/features/config/view/multiple_config_sources.feature
@@ -20,6 +20,7 @@ Feature: show the configuration
     Given the main branch is "git-main"
     And Git setting "git-town.perennial-branches" is "git-perennial-1 git-perennial-2"
     And Git setting "git-town.contribution-regex" is "^git-contribution-regex"
+    And Git setting "git-town.detached" is "true"
     And Git setting "git-town.observed-regex" is "^git-observed-regex"
     And Git setting "git-town.perennial-regex" is "^git-perennial-"
     And Git setting "git-town.feature-regex" is "git-feature-.*"
@@ -57,6 +58,7 @@ Feature: show the configuration
       strategy = "api"
 
       [sync]
+      detached = false
       feature-strategy = "merge"
       perennial-strategy = "rebase"
       prototype-strategy = "rebase"
@@ -105,6 +107,7 @@ Feature: show the configuration
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: yes
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: ff-only
diff --git a/features/config/view/no_config.feature b/features/config/view/no_config.feature
index d388c584a..3c0eb27ae 100644
--- a/features/config/view/no_config.feature
+++ b/features/config/view/no_config.feature
@@ -59,6 +59,7 @@ Feature: no configuration data
 
       Sync:
         auto-resolve phantom conflicts: yes
+        run detached: no
         run pre-push hook: yes
         feature sync strategy: merge
         perennial sync strategy: rebase
diff --git a/features/delete/unconfigured/unconfigured.feature b/features/delete/unconfigured/unconfigured.feature
index 690da5a48..02d3c3efb 100644
--- a/features/delete/unconfigured/unconfigured.feature
+++ b/features/delete/unconfigured/unconfigured.feature
@@ -23,6 +23,7 @@ Feature: ask for missing configuration
       | sync prototype strategy     | enter |
       | sync upstream               | enter |
       | sync tags                   | enter |
+      | detached                    | enter |
       | share new branches          | enter |
       | push hook                   | enter |
       | ship strategy               | enter |
diff --git a/features/hack/give_non_existing_branch/on_feature_branch/unconfigured/unconfigured.feature b/features/hack/give_non_existing_branch/on_feature_branch/unconfigured/unconfigured.feature
index 2f0b3fe5d..ad3ddf16b 100644
--- a/features/hack/give_non_existing_branch/on_feature_branch/unconfigured/unconfigured.feature
+++ b/features/hack/give_non_existing_branch/on_feature_branch/unconfigured/unconfigured.feature
@@ -23,6 +23,7 @@ Feature: missing configuration
       | sync prototype strategy     | enter |
       | sync upstream               | enter |
       | sync tags                   | enter |
+      | detached                    | enter |
       | share new branches          | enter |
       | push hook                   | enter |
       | ship strategy               | enter |
@@ -33,6 +34,7 @@ Feature: missing configuration
     And Git Town runs the commands
       | BRANCH | COMMAND                                              |
       | main   | git fetch --prune --tags                             |
+      |        | git config git-town.detached false                   |
       |        | git config git-town.new-branch-type feature          |
       |        | git config git-town.main-branch main                 |
       |        | git config git-town.unknown-branch-type feature      |
diff --git a/features/propose/unconfigured/unconfigured.feature b/features/propose/unconfigured/unconfigured.feature
index f99e15ea9..eaa7e7bf1 100644
--- a/features/propose/unconfigured/unconfigured.feature
+++ b/features/propose/unconfigured/unconfigured.feature
@@ -27,6 +27,7 @@ Feature: ask for missing configuration
       | sync prototype strategy     | enter           |
       | sync upstream               | enter           |
       | sync tags                   | enter           |
+      | detached                    | enter           |
       | share new branches          | enter           |
       | push hook                   | enter           |
       | ship strategy               | enter           |
diff --git a/features/ship/always_merge/current_branch/unconfigured/unconfigured.feature b/features/ship/always_merge/current_branch/unconfigured/unconfigured.feature
index b2500c67a..7c7005f30 100644
--- a/features/ship/always_merge/current_branch/unconfigured/unconfigured.feature
+++ b/features/ship/always_merge/current_branch/unconfigured/unconfigured.feature
@@ -24,6 +24,7 @@ Feature: ask for missing configuration information
       | sync prototype strategy     | enter |
       | sync upstream               | enter |
       | sync tags                   | enter |
+      | detached                    | enter |
       | share new branches          | enter |
       | push hook                   | enter |
       | ship strategy               | enter |
diff --git a/features/ship/fast_forward/current_branch/unconfigured/unconfigured.feature b/features/ship/fast_forward/current_branch/unconfigured/unconfigured.feature
index cc662da08..b06839d6d 100644
--- a/features/ship/fast_forward/current_branch/unconfigured/unconfigured.feature
+++ b/features/ship/fast_forward/current_branch/unconfigured/unconfigured.feature
@@ -24,6 +24,7 @@ Feature: ask for missing configuration information
       | sync prototype strategy     | enter |
       | sync upstream               | enter |
       | sync tags                   | enter |
+      | detached                    | enter |
       | share new branches          | enter |
       | push hook                   | enter |
       | ship strategy               | enter |
diff --git a/features/ship/squash_merge/current_branch/unconfigured/unconfigured.feature b/features/ship/squash_merge/current_branch/unconfigured/unconfigured.feature
index 387b38861..10e427ecc 100644
--- a/features/ship/squash_merge/current_branch/unconfigured/unconfigured.feature
+++ b/features/ship/squash_merge/current_branch/unconfigured/unconfigured.feature
@@ -24,6 +24,7 @@ Feature: ask for missing configuration information
       | sync prototype strategy     | enter |
       | sync upstream               | enter |
       | sync tags                   | enter |
+      | detached                    | enter |
       | share new branches          | enter |
       | push hook                   | enter |
       | ship strategy               | enter |
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main.feature
deleted file mode 100644
index aadde4050..000000000
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main.feature
+++ /dev/null
@@ -1,29 +0,0 @@
-Feature: sync the current feature branch with a tracking branch in detached mode with updates on main
-
-  Background:
-    Given a Git repo with origin
-    And the branches
-      | NAME  | TYPE    | PARENT | LOCATIONS     |
-      | alpha | feature | main   | local, origin |
-      | beta  | feature | alpha  | local, origin |
-    And the commits
-      | BRANCH | LOCATION      | MESSAGE     |
-      | main   | local, origin | main commit |
-    And the current branch is "beta"
-    When I run "git-town sync --detached"
-
-  Scenario: result
-    Then Git Town runs the commands
-      | BRANCH | COMMAND                  |
-      | beta   | git fetch --prune --tags |
-      |        | git checkout alpha       |
-      | alpha  | git checkout beta        |
-    And these commits exist now
-      | BRANCH | LOCATION      | MESSAGE     |
-      | main   | local, origin | main commit |
-
-  Scenario: undo
-    When I run "git-town undo"
-    Then Git Town runs no commands
-    And the initial commits exist now
-    And the initial branches and lineage exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/no_updates.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/cli_flag.feature
similarity index 51%
rename from features/sync/current_branch/feature_branch/merge_sync_strategy/detached/no_updates.feature
rename to features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/cli_flag.feature
index a1f1a0f3b..ef0b41756 100644
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/no_updates.feature
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/cli_flag.feature
@@ -1,35 +1,36 @@
-Feature: sync the current feature branch with a tracking branch in detached mode with no updates
+Feature: sync the current feature branch with a tracking branch in detached mode with updates on main
 
   Background:
     Given a Git repo with origin
     And the branches
       | NAME  | TYPE    | PARENT | LOCATIONS     |
       | alpha | feature | main   | local, origin |
+      | beta  | feature | alpha  | local, origin |
     And the commits
       | BRANCH | LOCATION      | MESSAGE      |
+      | main   | local, origin | main commit  |
       | alpha  | local, origin | alpha commit |
-    And the branches
-      | NAME | TYPE    | PARENT | LOCATIONS     |
-      | beta | feature | alpha  | local, origin |
-    And the commits
-      | BRANCH | LOCATION      | MESSAGE     |
-      | beta   | local, origin | beta commit |
     And the current branch is "beta"
     When I run "git-town sync --detached"
 
   Scenario: result
     Then Git Town runs the commands
-      | BRANCH | COMMAND                  |
-      | beta   | git fetch --prune --tags |
-      |        | git checkout alpha       |
-      | alpha  | git checkout beta        |
+      | BRANCH | COMMAND                        |
+      | beta   | git fetch --prune --tags       |
+      |        | git checkout alpha             |
+      | alpha  | git checkout beta              |
+      | beta   | git merge --no-edit --ff alpha |
+      |        | git push                       |
     And these commits exist now
       | BRANCH | LOCATION      | MESSAGE      |
+      | main   | local, origin | main commit  |
       | alpha  | local, origin | alpha commit |
-      | beta   | local, origin | beta commit  |
 
   Scenario: undo
     When I run "git-town undo"
-    Then Git Town runs no commands
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                                         |
+      | beta   | git reset --hard {{ sha 'initial commit' }}     |
+      |        | git push --force-with-lease --force-if-includes |
     And the initial commits exist now
     And the initial branches and lineage exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/config_file.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/config_file.feature
new file mode 100644
index 000000000..da0e7a9e6
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/config_file.feature
@@ -0,0 +1,42 @@
+Feature: sync the current feature branch with a tracking branch in detached mode with updates on main
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME  | TYPE    | PARENT | LOCATIONS     |
+      | alpha | feature | main   | local, origin |
+      | beta  | feature | alpha  | local, origin |
+    And the commits
+      | BRANCH | LOCATION      | MESSAGE      |
+      | main   | local, origin | main commit  |
+      | alpha  | local, origin | alpha commit |
+    And the current branch is "beta"
+    And the committed configuration file:
+      """
+      [sync]
+      detached = true
+      """
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                        |
+      | beta   | git fetch --prune --tags       |
+      |        | git checkout alpha             |
+      | alpha  | git checkout beta              |
+      | beta   | git merge --no-edit --ff alpha |
+      |        | git push                       |
+    And these commits exist now
+      | BRANCH | LOCATION      | MESSAGE                        |
+      | main   | local, origin | main commit                    |
+      | alpha  | local, origin | alpha commit                   |
+      | beta   | local, origin | Merge branch 'alpha' into beta |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                                                    |
+      | beta   | git reset --hard {{ sha-initial 'persisted config file' }} |
+      |        | git push --force-with-lease --force-if-includes            |
+    And the initial commits exist now
+    And the initial branches and lineage exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/disable_via_cli.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/disable_via_cli.feature
new file mode 100644
index 000000000..ff07b1e3f
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/disable_via_cli.feature
@@ -0,0 +1,41 @@
+Feature: sync the current feature branch with a tracking branch in detached mode with updates on main
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME  | TYPE    | PARENT | LOCATIONS     |
+      | alpha | feature | main   | local, origin |
+      | beta  | feature | alpha  | local, origin |
+    And the commits
+      | BRANCH | LOCATION      | MESSAGE     |
+      | main   | local, origin | main commit |
+    And the current branch is "beta"
+    And Git setting "git-town.detached" is "true"
+    When I run "git-town sync --no-detached"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                        |
+      | beta   | git fetch --prune --tags       |
+      |        | git checkout alpha             |
+      | alpha  | git merge --no-edit --ff main  |
+      |        | git push                       |
+      |        | git checkout beta              |
+      | beta   | git merge --no-edit --ff alpha |
+      |        | git push                       |
+    And these commits exist now
+      | BRANCH | LOCATION      | MESSAGE     |
+      | main   | local, origin | main commit |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                                         |
+      | beta   | git checkout alpha                              |
+      | alpha  | git reset --hard {{ sha 'initial commit` }}     |
+      |        | git push --force-with-lease --force-if-includes |
+      |        | git checkout beta                               |
+      | beta   | git reset --hard {{ sha 'initial commit` }}     |
+      |        | git push --force-with-lease --force-if-includes |
+    And the initial commits exist now
+    And the initial branches and lineage exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/git_metadata.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/git_metadata.feature
new file mode 100644
index 000000000..3ad83e963
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/detached/updates_on_main/git_metadata.feature
@@ -0,0 +1,37 @@
+Feature: sync the current feature branch with a tracking branch in detached mode with updates on main
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME  | TYPE    | PARENT | LOCATIONS     |
+      | alpha | feature | main   | local, origin |
+      | beta  | feature | alpha  | local, origin |
+    And the commits
+      | BRANCH | LOCATION      | MESSAGE      |
+      | main   | local, origin | main commit  |
+      | alpha  | local, origin | alpha commit |
+    And the current branch is "beta"
+    And Git setting "git-town.detached" is "true"
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                        |
+      | beta   | git fetch --prune --tags       |
+      |        | git checkout alpha             |
+      | alpha  | git checkout beta              |
+      | beta   | git merge --no-edit --ff alpha |
+      |        | git push                       |
+    And these commits exist now
+      | BRANCH | LOCATION      | MESSAGE      |
+      | main   | local, origin | main commit  |
+      | alpha  | local, origin | alpha commit |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                                         |
+      | beta   | git reset --hard {{ sha 'initial commit' }}     |
+      |        | git push --force-with-lease --force-if-includes |
+    And the initial commits exist now
+    And the initial branches and lineage exist now
diff --git a/features/sync/features/unconfigured.feature b/features/sync/features/unconfigured.feature
index dd22784e6..270597aa6 100644
--- a/features/sync/features/unconfigured.feature
+++ b/features/sync/features/unconfigured.feature
@@ -23,6 +23,7 @@ Feature: ask for missing configuration information
       | sync prototype strategy     | enter |
       | sync upstream               | enter |
       | sync tags                   | enter |
+      | detached                    | enter |
       | share new branches          | enter |
       | push hook                   | enter |
       | ship strategy               | enter |
diff --git a/internal/cli/dialog/detached.go b/internal/cli/dialog/detached.go
new file mode 100644
index 000000000..d43f5315d
--- /dev/null
+++ b/internal/cli/dialog/detached.go
@@ -0,0 +1,53 @@
+package dialog
+
+import (
+	"fmt"
+
+	"github.com/git-town/git-town/v21/internal/cli/dialog/dialogcomponents"
+	"github.com/git-town/git-town/v21/internal/cli/dialog/dialogcomponents/list"
+	"github.com/git-town/git-town/v21/internal/cli/dialog/dialogdomain"
+	"github.com/git-town/git-town/v21/internal/config/configdomain"
+	"github.com/git-town/git-town/v21/internal/messages"
+	. "github.com/git-town/git-town/v21/pkg/prelude"
+)
+
+const (
+	detachedTitle = `Sync in detached mode?`
+	detachedHelp  = `
+By default, "git town sync" pulls
+the latest commits from the main branch
+into your feature branches.
+
+Detached mode prevents this.  When enabled,
+feature branches sync only with their tracking
+and non-perennial parent branches.
+
+To manually pull new commits from the main branch
+in detached mode, run "git town sync --no-detached".
+
+`
+)
+
+func SyncDetached(args Args[configdomain.Detached]) (Option[configdomain.Detached], dialogdomain.Exit, error) {
+	entries := list.Entries[Option[configdomain.Detached]]{}
+	if global, hasGlobal := args.Global.Get(); hasGlobal {
+		entries = append(entries, list.Entry[Option[configdomain.Detached]]{
+			Data: None[configdomain.Detached](),
+			Text: fmt.Sprintf(messages.DialogUseGlobalValue, global),
+		})
+	}
+	entries = append(entries, list.Entries[Option[configdomain.Detached]]{
+		{
+			Data: Some(configdomain.Detached(false)),
+			Text: "disabled, sync pulls updates from the perennial root",
+		},
+		{
+			Data: Some(configdomain.Detached(true)),
+			Text: "enabled, sync does not pull updates from the perennial root",
+		},
+	}...)
+	defaultPos := entries.IndexOf(args.Local)
+	selection, exit, err := dialogcomponents.RadioList(entries, defaultPos, detachedTitle, detachedHelp, args.Inputs, "detached")
+	fmt.Printf(messages.DetachedResult, dialogcomponents.FormattedOption(selection, args.Global.IsSome(), exit))
+	return selection, exit, err
+}
diff --git a/internal/cli/flags/detached.go b/internal/cli/flags/detached.go
index 3563531b1..da4c5ee6a 100644
--- a/internal/cli/flags/detached.go
+++ b/internal/cli/flags/detached.go
@@ -2,22 +2,34 @@ package flags
 
 import (
 	"github.com/git-town/git-town/v21/internal/config/configdomain"
+	. "github.com/git-town/git-town/v21/pkg/prelude"
 	"github.com/spf13/cobra"
 )
 
-const detachedLong = "detached"
+const (
+	detachedLong = "detached"
+	noDetached   = "no-" + detachedLong
+)
 
 // type-safe access to the CLI arguments of type configdomain.Detached
 func Detached() (AddFunc, ReadDetachedFlagFunc) {
 	addFlag := func(cmd *cobra.Command) {
 		cmd.Flags().BoolP(detachedLong, "d", false, "don't update the perennial root branch")
+		cmd.Flags().Bool(noDetached, true, "")
 	}
-	readFlag := func(cmd *cobra.Command) (configdomain.Detached, error) {
-		value, err := cmd.Flags().GetBool(detachedLong)
-		return configdomain.Detached(value), err
+	readFlag := func(cmd *cobra.Command) (Option[configdomain.Detached], error) {
+		if cmd.Flags().Changed(detachedLong) {
+			value, err := cmd.Flags().GetBool(detachedLong)
+			return Some(configdomain.Detached(value)), err
+		}
+		if cmd.Flags().Changed(noDetached) {
+			value, err := cmd.Flags().GetBool(noDetached)
+			return Some(configdomain.Detached(!value)), err
+		}
+		return None[configdomain.Detached](), nil
 	}
 	return addFlag, readFlag
 }
 
 // ReadDetachedFlagFunc is the type signature for the function that reads the "detached" flag from the args to the given Cobra command.
-type ReadDetachedFlagFunc func(*cobra.Command) (configdomain.Detached, error)
+type ReadDetachedFlagFunc func(*cobra.Command) (Option[configdomain.Detached], error)
diff --git a/internal/cmd/append.go b/internal/cmd/append.go
index ac748a048..f55a07825 100644
--- a/internal/cmd/append.go
+++ b/internal/cmd/append.go
@@ -97,6 +97,7 @@ func appendCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: autoResolve,
+				Detached:    detached,
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -106,7 +107,6 @@ func appendCmd() *cobra.Command {
 				cliConfig:     cliConfig,
 				commit:        commit,
 				commitMessage: commitMessage,
-				detached:      detached,
 				propose:       propose,
 				prototype:     prototype,
 			})
@@ -130,7 +130,6 @@ type executeAppendArgs struct {
 	cliConfig     configdomain.PartialConfig
 	commit        configdomain.Commit
 	commitMessage Option[gitdomain.CommitMessage]
-	detached      configdomain.Detached
 	propose       configdomain.Propose
 	prototype     configdomain.Prototype
 }
@@ -150,7 +149,6 @@ func executeAppend(args executeAppendArgs) error {
 		beam:          args.beam,
 		commit:        args.commit,
 		commitMessage: args.commitMessage,
-		detached:      args.detached,
 		propose:       args.propose,
 		prototype:     args.prototype,
 		targetBranch:  gitdomain.NewLocalBranchName(args.arg),
@@ -178,7 +176,6 @@ func executeAppend(args executeAppendArgs) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                args.detached,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -205,7 +202,6 @@ type appendFeatureData struct {
 	commitsToBeam             gitdomain.Commits
 	config                    config.ValidatedConfig
 	connector                 Option[forgedomain.Connector]
-	detached                  configdomain.Detached
 	hasOpenChanges            bool
 	initialBranch             gitdomain.LocalBranchName
 	initialBranchInfo         *gitdomain.BranchInfo
@@ -255,7 +251,6 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              args.detached,
 		Fetch:                 !repoStatus.OpenChanges && args.beam.IsFalse() && args.commit.IsFalse(),
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
@@ -310,7 +305,7 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		return data, exit, err
 	}
 	branchNamesToSync := validatedConfig.NormalConfig.Lineage.BranchAndAncestors(initialBranch)
-	if args.detached {
+	if repo.UnvalidatedConfig.NormalConfig.Detached {
 		branchNamesToSync = validatedConfig.RemovePerennials(branchNamesToSync)
 	}
 	branchInfosToSync, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, branchNamesToSync...)
@@ -346,7 +341,6 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		commitsToBeam:             commitsToBeam,
 		config:                    validatedConfig,
 		connector:                 connector,
-		detached:                  args.detached,
 		hasOpenChanges:            repoStatus.OpenChanges,
 		initialBranch:             initialBranch,
 		initialBranchInfo:         initialBranchInfo,
@@ -367,7 +361,6 @@ type determineAppendDataArgs struct {
 	beam          configdomain.Beam
 	commit        configdomain.Commit
 	commitMessage Option[gitdomain.CommitMessage]
-	detached      configdomain.Detached
 	propose       configdomain.Propose
 	prototype     configdomain.Prototype
 	targetBranch  gitdomain.LocalBranchName
diff --git a/internal/cmd/branch.go b/internal/cmd/branch.go
index 32551e0b9..743bb3336 100644
--- a/internal/cmd/branch.go
+++ b/internal/cmd/branch.go
@@ -42,6 +42,7 @@ func branchCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -93,7 +94,6 @@ func determineBranchData(repo execute.OpenRepoResult) (data branchData, exit dia
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             None[forgedomain.Connector](),
-		Detached:              true,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/compress.go b/internal/cmd/compress.go
index d8e56b6aa..e02093429 100644
--- a/internal/cmd/compress.go
+++ b/internal/cmd/compress.go
@@ -83,6 +83,7 @@ func compressCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -136,7 +137,6 @@ func executeCompress(cliConfig configdomain.PartialConfig, message Option[gitdom
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               None[forgedomain.Connector](),
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -204,7 +204,6 @@ func determineCompressBranchesData(repo execute.OpenRepoResult, message Option[g
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/config/get_parent.go b/internal/cmd/config/get_parent.go
index 2e8c9fe23..12aebd23f 100644
--- a/internal/cmd/config/get_parent.go
+++ b/internal/cmd/config/get_parent.go
@@ -30,6 +30,7 @@ func getParentCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/config/remove.go b/internal/cmd/config/remove.go
index 98d1e7aff..7c379248e 100644
--- a/internal/cmd/config/remove.go
+++ b/internal/cmd/config/remove.go
@@ -31,6 +31,7 @@ func removeConfigCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/config/root.go b/internal/cmd/config/root.go
index ad6c6ef95..0e024fff9 100644
--- a/internal/cmd/config/root.go
+++ b/internal/cmd/config/root.go
@@ -33,6 +33,7 @@ func RootCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -102,6 +103,7 @@ func printConfig(config config.UnvalidatedConfig) {
 	fmt.Println()
 	print.Header("Sync")
 	print.Entry("auto-resolve phantom conflicts", format.Bool(config.NormalConfig.AutoResolve.ShouldAutoResolve()))
+	print.Entry("run detached", format.Bool(config.NormalConfig.Detached.IsTrue()))
 	print.Entry("run pre-push hook", format.Bool(config.NormalConfig.PushHook.IsTrue()))
 	print.Entry("feature sync strategy", config.NormalConfig.SyncFeatureStrategy.String())
 	print.Entry("perennial sync strategy", config.NormalConfig.SyncPerennialStrategy.String())
diff --git a/internal/cmd/config/setup.go b/internal/cmd/config/setup.go
index 3b043d9ae..b560a5770 100644
--- a/internal/cmd/config/setup.go
+++ b/internal/cmd/config/setup.go
@@ -35,6 +35,7 @@ func SetupCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -92,7 +93,6 @@ func LoadData(repo execute.OpenRepoResult) (data setup.Data, exit dialogdomain.E
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             None[forgedomain.Connector](),
-		Detached:              false,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/continue.go b/internal/cmd/continue.go
index 5c3b6d1a3..30f382fb6 100644
--- a/internal/cmd/continue.go
+++ b/internal/cmd/continue.go
@@ -43,6 +43,7 @@ func continueCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -77,7 +78,6 @@ func executeContinue(cliConfig configdomain.PartialConfig) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                false,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -123,7 +123,6 @@ func determineContinueData(repo execute.OpenRepoResult) (data continueData, exit
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              false,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/contribute.go b/internal/cmd/contribute.go
index 69a9c7948..671c47538 100644
--- a/internal/cmd/contribute.go
+++ b/internal/cmd/contribute.go
@@ -51,6 +51,7 @@ func contributeCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go
index 1d6b6e269..d41c579a1 100644
--- a/internal/cmd/delete.go
+++ b/internal/cmd/delete.go
@@ -81,6 +81,7 @@ func deleteCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -132,7 +133,6 @@ func executeDelete(args []string, cliConfig configdomain.PartialConfig) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -195,7 +195,6 @@ func determineDeleteData(args []string, repo execute.OpenRepoResult) (data delet
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/detach.go b/internal/cmd/detach.go
index 43a5f67a6..7340aae19 100644
--- a/internal/cmd/detach.go
+++ b/internal/cmd/detach.go
@@ -85,6 +85,7 @@ func detachCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -135,7 +136,6 @@ func executeDetach(args []string, cliConfig configdomain.PartialConfig) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -206,7 +206,6 @@ func determineDetachData(args []string, repo execute.OpenRepoResult) (data detac
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/diff_parent.go b/internal/cmd/diff_parent.go
index bf64821f6..f4261504c 100644
--- a/internal/cmd/diff_parent.go
+++ b/internal/cmd/diff_parent.go
@@ -45,6 +45,7 @@ func diffParentCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -114,7 +115,6 @@ func determineDiffParentData(args []string, repo execute.OpenRepoResult) (data d
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/down.go b/internal/cmd/down.go
index 83a42df72..a6d827109 100644
--- a/internal/cmd/down.go
+++ b/internal/cmd/down.go
@@ -39,6 +39,7 @@ func downCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/hack.go b/internal/cmd/hack.go
index 4d7078982..14e0e00ab 100644
--- a/internal/cmd/hack.go
+++ b/internal/cmd/hack.go
@@ -99,6 +99,7 @@ func hackCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: autoResolve,
+				Detached:    detached,
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -108,7 +109,6 @@ func hackCmd() *cobra.Command {
 				cliConfig:     cliConfig,
 				commit:        commit,
 				commitMessage: commitMessage,
-				detached:      detached,
 				propose:       propose,
 				prototype:     prototype,
 			})
@@ -132,7 +132,6 @@ type hackArgs struct {
 	cliConfig     configdomain.PartialConfig
 	commit        configdomain.Commit
 	commitMessage Option[gitdomain.CommitMessage]
-	detached      configdomain.Detached
 	propose       configdomain.Propose
 	prototype     configdomain.Prototype
 }
@@ -226,7 +225,6 @@ func createFeatureBranch(args createFeatureBranchArgs) error {
 		CommandsCounter:         args.commandsCounter,
 		Config:                  args.appendData.config,
 		Connector:               args.appendData.connector,
-		Detached:                args.appendData.detached,
 		FinalMessages:           args.finalMessages,
 		Frontend:                args.frontend,
 		Git:                     args.git,
@@ -279,7 +277,6 @@ func determineHackData(args hackArgs, repo execute.OpenRepoResult) (data hackDat
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              args.detached,
 		Fetch:                 len(args.argv) == 1 && !repoStatus.OpenChanges && args.beam.IsFalse() && args.commit.IsFalse(),
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
@@ -356,7 +353,7 @@ func determineHackData(args hackArgs, repo execute.OpenRepoResult) (data hackDat
 		return data, false, fmt.Errorf(messages.BranchAlreadyExistsRemotely, targetBranch)
 	}
 	branchNamesToSync := gitdomain.LocalBranchNames{validatedConfig.ValidatedConfigData.MainBranch}
-	if args.detached {
+	if validatedConfig.NormalConfig.Detached {
 		branchNamesToSync = validatedConfig.RemovePerennials(branchNamesToSync)
 	}
 	branchInfosToSync, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, branchNamesToSync...)
@@ -390,7 +387,6 @@ func determineHackData(args hackArgs, repo execute.OpenRepoResult) (data hackDat
 		commitsToBeam:             commitsToBeam,
 		config:                    validatedConfig,
 		connector:                 connector,
-		detached:                  args.detached,
 		hasOpenChanges:            repoStatus.OpenChanges,
 		initialBranch:             initialBranch,
 		initialBranchInfo:         initialBranchInfo,
diff --git a/internal/cmd/kill.go b/internal/cmd/kill.go
index 953631c9d..5709d7949 100644
--- a/internal/cmd/kill.go
+++ b/internal/cmd/kill.go
@@ -32,6 +32,7 @@ func killCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/merge.go b/internal/cmd/merge.go
index adc7d4670..af6954c8d 100644
--- a/internal/cmd/merge.go
+++ b/internal/cmd/merge.go
@@ -80,6 +80,7 @@ func mergeCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -129,7 +130,6 @@ func executeMerge(cliConfig configdomain.PartialConfig) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               None[forgedomain.Connector](),
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -193,7 +193,6 @@ func determineMergeData(repo execute.OpenRepoResult) (mergeData, dialogdomain.Ex
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/observe.go b/internal/cmd/observe.go
index bf6c906ac..7fd67c023 100644
--- a/internal/cmd/observe.go
+++ b/internal/cmd/observe.go
@@ -50,6 +50,7 @@ func observeCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/offline.go b/internal/cmd/offline.go
index c425a3308..b911e8437 100644
--- a/internal/cmd/offline.go
+++ b/internal/cmd/offline.go
@@ -41,6 +41,7 @@ func offlineCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/park.go b/internal/cmd/park.go
index 6f3b7fd5a..145a29b8f 100644
--- a/internal/cmd/park.go
+++ b/internal/cmd/park.go
@@ -44,6 +44,7 @@ func parkCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/prepend.go b/internal/cmd/prepend.go
index f9bc92034..aa785a694 100644
--- a/internal/cmd/prepend.go
+++ b/internal/cmd/prepend.go
@@ -106,6 +106,7 @@ func prependCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: autoResolve,
+				Detached:    detached,
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -115,7 +116,6 @@ func prependCommand() *cobra.Command {
 				cliConfig:     cliConfig,
 				commit:        commit,
 				commitMessage: commitMessage,
-				detached:      detached,
 				proposalBody:  bodyText,
 				proposalTitle: title,
 				propose:       propose,
@@ -143,7 +143,6 @@ type prependArgs struct {
 	cliConfig     configdomain.PartialConfig
 	commit        configdomain.Commit
 	commitMessage Option[gitdomain.CommitMessage]
-	detached      configdomain.Detached
 	proposalBody  Option[gitdomain.ProposalBody]
 	proposalTitle Option[gitdomain.ProposalTitle]
 	propose       configdomain.Propose
@@ -185,7 +184,6 @@ func executePrepend(args prependArgs) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                args.detached,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -265,7 +263,6 @@ func determinePrependData(args prependArgs, repo execute.OpenRepoResult) (data p
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              args.detached,
 		Fetch:                 !repoStatus.OpenChanges && args.beam.IsFalse() && args.commit.IsFalse(),
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
@@ -338,7 +335,7 @@ func determinePrependData(args prependArgs, repo execute.OpenRepoResult) (data p
 		}
 	}
 	branchNamesToSync := validatedConfig.NormalConfig.Lineage.BranchAndAncestors(initialBranch)
-	if args.detached {
+	if repo.UnvalidatedConfig.NormalConfig.Detached {
 		branchNamesToSync = validatedConfig.RemovePerennials(branchNamesToSync)
 	}
 	branchInfosToSync, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, branchNamesToSync...)
diff --git a/internal/cmd/propose.go b/internal/cmd/propose.go
index b5136d5e0..ffbda6e61 100644
--- a/internal/cmd/propose.go
+++ b/internal/cmd/propose.go
@@ -81,6 +81,7 @@ func proposeCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: autoResolve,
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -146,7 +147,6 @@ func executePropose(args proposeArgs) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -223,7 +223,6 @@ func determineProposeData(repo execute.OpenRepoResult, args proposeArgs) (data p
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connectorOpt,
-		Detached:              true,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/prototype.go b/internal/cmd/prototype.go
index 7abb4261f..990028ca4 100644
--- a/internal/cmd/prototype.go
+++ b/internal/cmd/prototype.go
@@ -47,6 +47,7 @@ func prototypeCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/rename.go b/internal/cmd/rename.go
index 1b2bfe524..ed8944902 100644
--- a/internal/cmd/rename.go
+++ b/internal/cmd/rename.go
@@ -58,6 +58,7 @@ func renameCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -105,7 +106,6 @@ func executeRename(args []string, cliConfig configdomain.PartialConfig, force co
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -169,7 +169,6 @@ func determineRenameData(args []string, force configdomain.Force, repo execute.O
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/repo.go b/internal/cmd/repo.go
index 9823df6d4..04f7f672a 100644
--- a/internal/cmd/repo.go
+++ b/internal/cmd/repo.go
@@ -45,6 +45,7 @@ func repoCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/runlog.go b/internal/cmd/runlog.go
index 2ba2ac061..6ec0b6825 100644
--- a/internal/cmd/runlog.go
+++ b/internal/cmd/runlog.go
@@ -45,6 +45,7 @@ func runLogCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/set_parent.go b/internal/cmd/set_parent.go
index e38dfab2f..0123a9954 100644
--- a/internal/cmd/set_parent.go
+++ b/internal/cmd/set_parent.go
@@ -76,6 +76,7 @@ func setParentCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: autoResolve,
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -178,7 +179,6 @@ func executeSetParent(args []string, cliConfig configdomain.PartialConfig) error
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -237,7 +237,6 @@ func determineSetParentData(repo execute.OpenRepoResult) (data setParentData, ex
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/ship/cmd.go b/internal/cmd/ship/cmd.go
index f7df2bbb4..21076aaa7 100644
--- a/internal/cmd/ship/cmd.go
+++ b/internal/cmd/ship/cmd.go
@@ -70,6 +70,7 @@ func Cmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -177,7 +178,6 @@ func executeShip(args executeShipArgs) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  sharedData.config,
 		Connector:               sharedData.connector,
-		Detached:                false,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
diff --git a/internal/cmd/ship/shared.go b/internal/cmd/ship/shared.go
index 850f14f49..c23ad4c41 100644
--- a/internal/cmd/ship/shared.go
+++ b/internal/cmd/ship/shared.go
@@ -70,7 +70,6 @@ func determineSharedShipData(args []string, repo execute.OpenRepoResult, shipStr
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              false,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/skip.go b/internal/cmd/skip.go
index 36e855753..8076fe32b 100644
--- a/internal/cmd/skip.go
+++ b/internal/cmd/skip.go
@@ -38,6 +38,7 @@ func skipCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -88,7 +89,6 @@ func executeSkip(cliConfig configdomain.PartialConfig) error {
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              false,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
@@ -153,7 +153,6 @@ func executeSkip(cliConfig configdomain.PartialConfig) error {
 		CommandsCounter: repo.CommandsCounter,
 		Config:          validatedConfig,
 		Connector:       connector,
-		Detached:        false,
 		FinalMessages:   repo.FinalMessages,
 		Frontend:        repo.Frontend,
 		Git:             repo.Git,
diff --git a/internal/cmd/status/reset.go b/internal/cmd/status/reset.go
index 5e4afa0cb..3e0c999e3 100644
--- a/internal/cmd/status/reset.go
+++ b/internal/cmd/status/reset.go
@@ -35,6 +35,7 @@ func resetRunstateCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/status/root.go b/internal/cmd/status/root.go
index 180fd88c6..fe9ae20f7 100644
--- a/internal/cmd/status/root.go
+++ b/internal/cmd/status/root.go
@@ -38,6 +38,7 @@ func RootCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/status/show.go b/internal/cmd/status/show.go
index 10cee07c1..684106a52 100644
--- a/internal/cmd/status/show.go
+++ b/internal/cmd/status/show.go
@@ -29,6 +29,7 @@ func showRunstateCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/swap/cmd.go b/internal/cmd/swap/cmd.go
index f1d59ed3d..dc855ec1a 100644
--- a/internal/cmd/swap/cmd.go
+++ b/internal/cmd/swap/cmd.go
@@ -81,6 +81,7 @@ func Cmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: autoResolve,
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -132,7 +133,6 @@ func executeSwap(args []string, cliConfig configdomain.PartialConfig) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -205,7 +205,6 @@ func determineSwapData(args []string, repo execute.OpenRepoResult) (data swapDat
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/switch.go b/internal/cmd/switch.go
index 74ef6edbd..9f8004504 100644
--- a/internal/cmd/switch.go
+++ b/internal/cmd/switch.go
@@ -49,6 +49,7 @@ func switchCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -161,7 +162,6 @@ func determineSwitchData(args []string, repo execute.OpenRepoResult) (data switc
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             None[forgedomain.Connector](),
-		Detached:              true,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/sync/cmd.go b/internal/cmd/sync/cmd.go
index 923895447..b78f91fa8 100644
--- a/internal/cmd/sync/cmd.go
+++ b/internal/cmd/sync/cmd.go
@@ -80,12 +80,12 @@ func Cmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: autoResolve,
+				Detached:    detached,
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
 			return executeSync(executeSyncArgs{
 				cliConfig:       cliConfig,
-				detached:        detached,
 				prune:           prune,
 				pushBranches:    pushBranches,
 				stack:           stack,
@@ -106,7 +106,6 @@ func Cmd() *cobra.Command {
 
 type executeSyncArgs struct {
 	cliConfig       configdomain.PartialConfig
-	detached        configdomain.Detached
 	prune           configdomain.Prune
 	pushBranches    configdomain.PushBranches
 	stack           configdomain.FullStack
@@ -125,7 +124,6 @@ func executeSync(args executeSyncArgs) error {
 		return err
 	}
 	data, exit, err := determineSyncData(repo, determineSyncDataArgs{
-		detached:        args.detached,
 		syncAllBranches: args.syncAllBranches,
 		syncStack:       args.stack,
 	})
@@ -206,7 +204,6 @@ func executeSync(args executeSyncArgs) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                args.detached,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -228,7 +225,6 @@ type syncData struct {
 	branchesToSync           configdomain.BranchesToSync
 	config                   config.ValidatedConfig
 	connector                Option[forgedomain.Connector]
-	detached                 configdomain.Detached
 	hasOpenChanges           bool
 	initialBranch            gitdomain.LocalBranchName
 	inputs                   dialogcomponents.Inputs
@@ -242,7 +238,6 @@ type syncData struct {
 }
 
 type determineSyncDataArgs struct {
-	detached        configdomain.Detached
 	syncAllBranches configdomain.AllBranches
 	syncStack       configdomain.FullStack
 }
@@ -281,7 +276,6 @@ func determineSyncData(repo execute.OpenRepoResult, args determineSyncDataArgs)
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              args.detached,
 		Fetch:                 true,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
@@ -350,7 +344,7 @@ func determineSyncData(repo execute.OpenRepoResult, args determineSyncDataArgs)
 	perennialAndMain := branchesAndTypes.BranchesOfTypes(configdomain.BranchTypePerennialBranch, configdomain.BranchTypeMainBranch)
 	var branchNamesToSync gitdomain.LocalBranchNames
 	switch {
-	case args.syncAllBranches.Enabled() && args.detached.IsTrue():
+	case args.syncAllBranches.Enabled() && repo.UnvalidatedConfig.NormalConfig.Detached.IsTrue():
 		branchNamesToSync = localBranches.Remove(perennialAndMain...)
 	case args.syncAllBranches.Enabled():
 		branchNamesToSync = localBranches
@@ -388,7 +382,7 @@ func determineSyncData(repo execute.OpenRepoResult, args determineSyncDataArgs)
 		shouldPushTags = validatedConfig.IsMainOrPerennialBranch(initialBranch)
 	}
 	allBranchNamesToSync := validatedConfig.NormalConfig.Lineage.BranchesAndAncestors(branchNamesToSync)
-	if args.detached {
+	if repo.UnvalidatedConfig.NormalConfig.Detached {
 		allBranchNamesToSync = allBranchNamesToSync.Remove(perennialAndMain...)
 	}
 	branchInfosToSync, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, allBranchNamesToSync...)
@@ -402,7 +396,6 @@ func determineSyncData(repo execute.OpenRepoResult, args determineSyncDataArgs)
 		branchesToSync:           branchesToSync,
 		config:                   validatedConfig,
 		connector:                connector,
-		detached:                 args.detached,
 		hasOpenChanges:           repoStatus.OpenChanges,
 		initialBranch:            initialBranch,
 		inputs:                   inputs,
diff --git a/internal/cmd/undo.go b/internal/cmd/undo.go
index 922ed0b11..a7a67b0e7 100644
--- a/internal/cmd/undo.go
+++ b/internal/cmd/undo.go
@@ -41,6 +41,7 @@ func undoCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
@@ -80,7 +81,6 @@ func executeUndo(cliConfig configdomain.PartialConfig) error {
 		CommandsCounter:  repo.CommandsCounter,
 		Config:           data.config,
 		Connector:        data.connector,
-		Detached:         true,
 		FinalMessages:    repo.FinalMessages,
 		Frontend:         repo.Frontend,
 		Git:              repo.Git,
@@ -131,7 +131,6 @@ func determineUndoData(repo execute.OpenRepoResult) (data undoData, exit dialogd
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/cmd/up.go b/internal/cmd/up.go
index d5d9a16bd..decaa91fa 100644
--- a/internal/cmd/up.go
+++ b/internal/cmd/up.go
@@ -41,6 +41,7 @@ func upCmd() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    None[configdomain.Detached](),
 				DryRun:      None[configdomain.DryRun](),
 				Verbose:     verbose,
 			})
diff --git a/internal/cmd/walk.go b/internal/cmd/walk.go
index 0a441d8ad..ea168a41e 100644
--- a/internal/cmd/walk.go
+++ b/internal/cmd/walk.go
@@ -89,6 +89,7 @@ func walkCommand() *cobra.Command {
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
 				AutoResolve: None[configdomain.AutoResolve](),
+				Detached:    Some(configdomain.Detached(true)),
 				DryRun:      dryRun,
 				Verbose:     verbose,
 			})
@@ -155,7 +156,6 @@ func executeWalk(args executeWalkArgs) error {
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
 		Connector:               data.connector,
-		Detached:                true,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -214,7 +214,6 @@ func determineWalkData(repo execute.OpenRepoResult, all configdomain.AllBranches
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
 		Connector:             connector,
-		Detached:              true,
 		Fetch:                 false,
 		FinalMessages:         repo.FinalMessages,
 		Frontend:              repo.Frontend,
diff --git a/internal/config/cliconfig/cli_config.go b/internal/config/cliconfig/cli_config.go
index d8762ecfe..73ed179ef 100644
--- a/internal/config/cliconfig/cli_config.go
+++ b/internal/config/cliconfig/cli_config.go
@@ -9,6 +9,7 @@ import (
 
 type NewArgs struct {
 	AutoResolve Option[configdomain.AutoResolve]
+	Detached    Option[configdomain.Detached]
 	DryRun      Option[configdomain.DryRun]
 	Verbose     Option[configdomain.Verbose]
 }
@@ -22,6 +23,7 @@ func New(args NewArgs) configdomain.PartialConfig {
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{},
 		CodebergToken:            None[forgedomain.CodebergToken](),
 		ContributionRegex:        None[configdomain.ContributionRegex](),
+		Detached:                 args.Detached,
 		DevRemote:                None[gitdomain.Remote](),
 		DryRun:                   args.DryRun,
 		FeatureRegex:             None[configdomain.FeatureRegex](),
diff --git a/internal/config/configdomain/detached.go b/internal/config/configdomain/detached.go
index 8b27349ea..eec383832 100644
--- a/internal/config/configdomain/detached.go
+++ b/internal/config/configdomain/detached.go
@@ -1,5 +1,7 @@
 package configdomain
 
+import "strconv"
+
 // indicates whether a Git Town command should not update the root branch of the stack
 type Detached bool
 
@@ -10,3 +12,7 @@ func (self Detached) IsFalse() bool {
 func (self Detached) IsTrue() bool {
 	return bool(self)
 }
+
+func (self Detached) String() string {
+	return strconv.FormatBool(bool(self))
+}
diff --git a/internal/config/configdomain/key.go b/internal/config/configdomain/key.go
index eed0da80e..e04e9eaf3 100644
--- a/internal/config/configdomain/key.go
+++ b/internal/config/configdomain/key.go
@@ -79,6 +79,7 @@ const (
 	KeyDeprecatedPushVerify                = Key("git-town.push-verify")
 	KeyDeprecatedShipDeleteRemoteBranch    = Key("git-town.ship-delete-remote-branch")
 	KeyDeprecatedSyncStrategy              = Key("git-town.sync-strategy")
+	KeyDetached                            = Key("git-town.detached")
 	KeyDevRemote                           = Key("git-town.dev-remote")
 	KeyFeatureRegex                        = Key("git-town.feature-regex")
 	KeyForgeType                           = Key("git-town.forge-type")
@@ -136,6 +137,7 @@ var keys = []Key{
 	KeyDeprecatedPushVerify,
 	KeyDeprecatedShipDeleteRemoteBranch,
 	KeyDeprecatedSyncStrategy,
+	KeyDetached,
 	KeyDevRemote,
 	KeyFeatureRegex,
 	KeyForgeType,
diff --git a/internal/config/configdomain/partial_config.go b/internal/config/configdomain/partial_config.go
index 3fab5f778..de72d1c1c 100644
--- a/internal/config/configdomain/partial_config.go
+++ b/internal/config/configdomain/partial_config.go
@@ -23,6 +23,7 @@ type PartialConfig struct {
 	BranchTypeOverrides      BranchTypeOverrides
 	CodebergToken            Option[forgedomain.CodebergToken]
 	ContributionRegex        Option[ContributionRegex]
+	Detached                 Option[Detached]
 	DevRemote                Option[gitdomain.Remote]
 	DryRun                   Option[DryRun]
 	FeatureRegex             Option[FeatureRegex]
@@ -73,6 +74,7 @@ func (self PartialConfig) Merge(other PartialConfig) PartialConfig {
 		BranchTypeOverrides:      other.BranchTypeOverrides.Concat(self.BranchTypeOverrides),
 		CodebergToken:            other.CodebergToken.Or(self.CodebergToken),
 		ContributionRegex:        other.ContributionRegex.Or(self.ContributionRegex),
+		Detached:                 other.Detached.Or(self.Detached),
 		DevRemote:                other.DevRemote.Or(self.DevRemote),
 		DryRun:                   other.DryRun.Or(self.DryRun),
 		FeatureRegex:             other.FeatureRegex.Or(self.FeatureRegex),
diff --git a/internal/config/configfile/data.go b/internal/config/configfile/data.go
index 9eca9cf87..0443514e8 100644
--- a/internal/config/configfile/data.go
+++ b/internal/config/configfile/data.go
@@ -63,6 +63,7 @@ type Ship struct {
 
 type Sync struct {
 	AutoResolve       *bool   `toml:"auto-resolve"`
+	Detached          *bool   `toml:"detached"`
 	FeatureStrategy   *string `toml:"feature-strategy"`
 	PerennialStrategy *string `toml:"perennial-strategy"`
 	PrototypeStrategy *string `toml:"prototype-strategy"`
diff --git a/internal/config/configfile/load.go b/internal/config/configfile/load.go
index 8f7029a3c..04eef55c9 100644
--- a/internal/config/configfile/load.go
+++ b/internal/config/configfile/load.go
@@ -47,6 +47,7 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 	var err error
 	var contributionRegex Option[configdomain.ContributionRegex]
 	var unknownBranchType Option[configdomain.UnknownBranchType]
+	var detached Option[configdomain.Detached]
 	var devRemote Option[gitdomain.Remote]
 	var featureRegex Option[configdomain.FeatureRegex]
 	var forgeType Option[forgedomain.ForgeType]
@@ -207,6 +208,9 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 		if data.Sync.AutoResolve != nil {
 			autoResolve = Some(configdomain.AutoResolve(*data.Sync.AutoResolve))
 		}
+		if data.Sync.Detached != nil {
+			detached = Some(configdomain.Detached(*data.Sync.Detached))
+		}
 		if data.Sync.FeatureStrategy != nil {
 			syncFeatureStrategy, err = configdomain.ParseSyncFeatureStrategy(*data.Sync.FeatureStrategy)
 			ec.Check(err)
@@ -236,6 +240,7 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{},
 		CodebergToken:            None[forgedomain.CodebergToken](),
 		ContributionRegex:        contributionRegex,
+		Detached:                 detached,
 		DryRun:                   None[configdomain.DryRun](),
 		UnknownBranchType:        unknownBranchType,
 		DevRemote:                devRemote,
diff --git a/internal/config/envconfig/load.go b/internal/config/envconfig/load.go
index 80377ac16..0955e2313 100644
--- a/internal/config/envconfig/load.go
+++ b/internal/config/envconfig/load.go
@@ -16,6 +16,7 @@ const (
 	bitbucketUserName        = "GIT_TOWN_BITBUCKET_USERNAME"
 	codebergToken            = "GIT_TOWN_CODEBERG_TOKEN"
 	contributionRegex        = "GIT_TOWN_CONTRIBUTION_REGEX"
+	detached                 = "GIT_TOWN_DETACHED"
 	devRemote                = "GIT_TOWN_DEV_REMOTE"
 	dryRun                   = "GIT_TOWN_DRY_RUN"
 	featureRegex             = "GIT_TOWN_FEATURE_REGEX"
@@ -49,6 +50,7 @@ const (
 func Load(env EnvVars) (configdomain.PartialConfig, error) {
 	autoResolve, errAutoResolve := gohacks.ParseBoolOpt[configdomain.AutoResolve](env.Get(autoResolve), autoResolve)
 	contributionRegex, errContribRegex := configdomain.ParseContributionRegex(env.Get(contributionRegex))
+	detached, errDetached := gohacks.ParseBoolOpt[configdomain.Detached](env.Get(detached), detached)
 	dryRun, errDryRun := gohacks.ParseBoolOpt[configdomain.DryRun](env.Get(dryRun), dryRun)
 	featureRegex, errFeatureRegex := configdomain.ParseFeatureRegex(env.Get(featureRegex))
 	forgeType, errForgeType := forgedomain.ParseForgeType(env.Get(forgeType))
@@ -73,6 +75,7 @@ func Load(env EnvVars) (configdomain.PartialConfig, error) {
 	err := cmp.Or(
 		errAutoResolve,
 		errContribRegex,
+		errDetached,
 		errDryRun,
 		errFeatureRegex,
 		errForgeType,
@@ -103,6 +106,7 @@ func Load(env EnvVars) (configdomain.PartialConfig, error) {
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{}, // not loaded from env vars
 		CodebergToken:            forgedomain.ParseCodebergToken(env.Get(codebergToken)),
 		ContributionRegex:        contributionRegex,
+		Detached:                 detached,
 		DevRemote:                gitdomain.NewRemote(env.Get(devRemote)),
 		DryRun:                   dryRun,
 		FeatureRegex:             featureRegex,
diff --git a/internal/config/gitconfig/high_level.go b/internal/config/gitconfig/high_level.go
index 783ccbed8..6c9fece63 100644
--- a/internal/config/gitconfig/high_level.go
+++ b/internal/config/gitconfig/high_level.go
@@ -58,6 +58,10 @@ func RemoveContributionRegex(runner subshelldomain.Runner) error {
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyContributionRegex)
 }
 
+func RemoveDetached(runner subshelldomain.Runner) error {
+	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyDetached)
+}
+
 func RemoveDevRemote(runner subshelldomain.Runner) error {
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyDevRemote)
 }
@@ -187,6 +191,10 @@ func SetContributionRegex(runner subshelldomain.Runner, regex configdomain.Contr
 	return SetConfigValue(runner, scope, configdomain.KeyContributionRegex, regex.String())
 }
 
+func SetDetached(runner subshelldomain.Runner, value configdomain.Detached, scope configdomain.ConfigScope) error {
+	return SetConfigValue(runner, scope, configdomain.KeyDetached, value.String())
+}
+
 func SetDevRemote(runner subshelldomain.Runner, remote gitdomain.Remote, scope configdomain.ConfigScope) error {
 	return SetConfigValue(runner, scope, configdomain.KeyDevRemote, remote.String())
 }
diff --git a/internal/config/normal_config.go b/internal/config/normal_config.go
index 2489d4133..646a3754d 100644
--- a/internal/config/normal_config.go
+++ b/internal/config/normal_config.go
@@ -36,6 +36,7 @@ type NormalConfig struct {
 	BranchTypeOverrides      configdomain.BranchTypeOverrides
 	CodebergToken            Option[forgedomain.CodebergToken]
 	ContributionRegex        Option[configdomain.ContributionRegex]
+	Detached                 configdomain.Detached
 	DevRemote                gitdomain.Remote
 	DryRun                   configdomain.DryRun // whether to only print the Git commands but not execute them
 	FeatureRegex             Option[configdomain.FeatureRegex]
@@ -88,6 +89,7 @@ func (self *NormalConfig) OverwriteWith(other configdomain.PartialConfig) Normal
 		BranchTypeOverrides:      other.BranchTypeOverrides.Concat(self.BranchTypeOverrides),
 		CodebergToken:            other.CodebergToken.Or(self.CodebergToken),
 		ContributionRegex:        other.ContributionRegex.Or(self.ContributionRegex),
+		Detached:                 other.Detached.GetOrElse(self.Detached),
 		DevRemote:                other.DevRemote.GetOrElse(self.DevRemote),
 		DryRun:                   other.DryRun.GetOrElse(self.DryRun),
 		FeatureRegex:             other.FeatureRegex.Or(self.FeatureRegex),
@@ -229,6 +231,7 @@ func DefaultNormalConfig() NormalConfig {
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{},
 		CodebergToken:            None[forgedomain.CodebergToken](),
 		ContributionRegex:        None[configdomain.ContributionRegex](),
+		Detached:                 false,
 		DevRemote:                gitdomain.RemoteOrigin,
 		DryRun:                   false,
 		FeatureRegex:             None[configdomain.FeatureRegex](),
@@ -270,6 +273,7 @@ func NewNormalConfigFromPartial(partial configdomain.PartialConfig, defaults Nor
 		BranchTypeOverrides:      partial.BranchTypeOverrides,
 		CodebergToken:            partial.CodebergToken,
 		ContributionRegex:        partial.ContributionRegex,
+		Detached:                 partial.Detached.GetOrElse(defaults.Detached),
 		DevRemote:                partial.DevRemote.GetOrElse(defaults.DevRemote),
 		DryRun:                   partial.DryRun.GetOrDefault(),
 		FeatureRegex:             partial.FeatureRegex,
diff --git a/internal/config/parse_snapshot.go b/internal/config/parse_snapshot.go
index 8705fffa2..6c25cef47 100644
--- a/internal/config/parse_snapshot.go
+++ b/internal/config/parse_snapshot.go
@@ -81,6 +81,7 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 	autoResolve, errAutoResolve := gohacks.ParseBoolOpt[configdomain.AutoResolve](snapshot[configdomain.KeyAutoResolve], configdomain.KeyAutoResolve.String())
 	branchTypeOverrides, errBranchTypeOverride := NewBranchTypeOverridesInSnapshot(snapshot, runner)
 	contributionRegex, errContributionRegex := configdomain.ParseContributionRegex(snapshot[configdomain.KeyContributionRegex])
+	detached, errDetached := gohacks.ParseBoolOpt[configdomain.Detached](snapshot[configdomain.KeyDetached], configdomain.KeyDetached.String())
 	featureRegex, errFeatureRegex := configdomain.ParseFeatureRegex(snapshot[configdomain.KeyFeatureRegex])
 	forgeType, errForgeType := forgedomain.ParseForgeType(snapshot[configdomain.KeyForgeType])
 	githubConnectorType, errGitHubConnectorType := forgedomain.ParseGitHubConnectorType(snapshot[configdomain.KeyGitHubConnectorType])
@@ -107,6 +108,7 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 		errAutoResolve,
 		errBranchTypeOverride,
 		errContributionRegex,
+		errDetached,
 		errFeatureRegex,
 		errForgeType,
 		errGitHubConnectorType,
@@ -136,6 +138,7 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 		BranchTypeOverrides:      branchTypeOverrides,
 		CodebergToken:            forgedomain.ParseCodebergToken(snapshot[configdomain.KeyCodebergToken]),
 		ContributionRegex:        contributionRegex,
+		Detached:                 detached,
 		DevRemote:                gitdomain.NewRemote(snapshot[configdomain.KeyDevRemote]),
 		DryRun:                   None[configdomain.DryRun](),
 		FeatureRegex:             featureRegex,
diff --git a/internal/execute/load_repo_snapshot.go b/internal/execute/load_repo_snapshot.go
index 3c70dbf92..5e46c667e 100644
--- a/internal/execute/load_repo_snapshot.go
+++ b/internal/execute/load_repo_snapshot.go
@@ -34,7 +34,6 @@ func LoadRepoSnapshot(args LoadRepoSnapshotArgs) (gitdomain.BranchesSnapshot, gi
 			Backend:           args.Repo.Backend,
 			CommandsCounter:   args.Repo.CommandsCounter,
 			Connector:         args.Connector,
-			Detached:          args.Detached,
 			FinalMessages:     args.Repo.FinalMessages,
 			Frontend:          args.Repo.Frontend,
 			Git:               args.Git,
@@ -80,7 +79,6 @@ type LoadRepoSnapshotArgs struct {
 	CommandsCounter       Mutable[gohacks.Counter]
 	ConfigSnapshot        configdomain.BeginConfigSnapshot
 	Connector             Option[forgedomain.Connector]
-	Detached              configdomain.Detached
 	Fetch                 bool
 	FinalMessages         stringslice.Collector
 	Frontend              subshelldomain.Runner
diff --git a/internal/messages/en.go b/internal/messages/en.go
index f6b816f6b..df833573d 100644
--- a/internal/messages/en.go
+++ b/internal/messages/en.go
@@ -103,6 +103,7 @@ Please upgrade to the new format: create.new-branch-type = "prototype"`
 
 	DeleteCannotDeleteMainBranch        = "you cannot delete the main branch"
 	DeleteCannotDeletePerennialBranches = "you cannot delete perennial branches"
+	DetachedResult                      = "Detached: %s\n"
 	DetachNeedsSync                     = "please sync this stack before detaching branches from it"
 	DetachNoParent                      = "cannot detach branches without parent"
 	DetachOtherWorkTree                 = "cannot detach because branch %q it is active in another worktree"
diff --git a/internal/setup/enter.go b/internal/setup/enter.go
index b080a62f6..739c5761e 100644
--- a/internal/setup/enter.go
+++ b/internal/setup/enter.go
@@ -182,6 +182,10 @@ EnterForgeData:
 	if err != nil || exit {
 		return emptyResult, exit, err
 	}
+	detached, exit, err := enterDetached(data)
+	if err != nil || exit {
+		return emptyResult, exit, err
+	}
 	shareNewBranches, exit, err := enterShareNewBranches(data)
 	if err != nil || exit {
 		return emptyResult, exit, err
@@ -210,6 +214,7 @@ EnterForgeData:
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{}, // the setup assistant doesn't ask for this
 		CodebergToken:            codebergToken,
 		ContributionRegex:        contributionRegex,
+		Detached:                 detached,
 		DevRemote:                devRemote,
 		DryRun:                   None[configdomain.DryRun](), // the setup assistant doesn't ask for this
 		FeatureRegex:             featureRegex,
@@ -326,6 +331,17 @@ func enterContributionRegex(data Data) (Option[configdomain.ContributionRegex],
 	})
 }
 
+func enterDetached(data Data) (Option[configdomain.Detached], dialogdomain.Exit, error) {
+	if data.Config.File.Detached.IsSome() {
+		return None[configdomain.Detached](), false, nil
+	}
+	return dialog.SyncDetached(dialog.Args[configdomain.Detached]{
+		Global: data.Config.GitGlobal.Detached,
+		Inputs: data.Inputs,
+		Local:  data.Config.GitLocal.Detached,
+	})
+}
+
 func enterDevRemote(data Data) (Option[gitdomain.Remote], dialogdomain.Exit, error) {
 	if data.Config.File.DevRemote.IsSome() {
 		return None[gitdomain.Remote](), false, nil
diff --git a/internal/setup/save.go b/internal/setup/save.go
index 876e3603b..e4f95e381 100644
--- a/internal/setup/save.go
+++ b/internal/setup/save.go
@@ -84,6 +84,9 @@ func saveAllToFile(userInput UserInput, gitConfig configdomain.PartialConfig, ru
 	if gitConfig.ContributionRegex.IsSome() {
 		_ = gitconfig.RemoveContributionRegex(runner)
 	}
+	if gitConfig.Detached.IsSome() {
+		_ = gitconfig.RemoveDetached(runner)
+	}
 	if gitConfig.DevRemote.IsSome() {
 		_ = gitconfig.RemoveDevRemote(runner)
 	}
@@ -143,6 +146,11 @@ func saveAllToFile(userInput UserInput, gitConfig configdomain.PartialConfig, ru
 
 func saveAllToGit(userInput UserInput, existingGitConfig configdomain.PartialConfig, configFile configdomain.PartialConfig, data Data, frontend subshelldomain.Runner) error {
 	fc := gohacks.ErrorCollector{}
+	if configFile.Detached.IsNone() {
+		fc.Check(
+			saveDetached(userInput.Data.Detached, existingGitConfig.Detached, frontend),
+		)
+	}
 	if configFile.NewBranchType.IsNone() {
 		fc.Check(
 			saveNewBranchType(userInput.Data.NewBranchType, existingGitConfig.NewBranchType, frontend),
@@ -295,6 +303,17 @@ func saveContributionRegex(valueToWriteToGit Option[configdomain.ContributionReg
 	return nil
 }
 
+func saveDetached(valueToWriteToGit Option[configdomain.Detached], valueAlreadyInGit Option[configdomain.Detached], runner subshelldomain.Runner) error {
+	if valueToWriteToGit.Equal(valueAlreadyInGit) {
+		return nil
+	}
+	if value, hasValue := valueToWriteToGit.Get(); hasValue {
+		return gitconfig.SetDetached(runner, value, configdomain.ConfigScopeLocal)
+	}
+	_ = gitconfig.RemoveDetached(runner)
+	return nil
+}
+
 func saveDevRemote(valueToWriteToGit Option[gitdomain.Remote], valueAlreadyInGit Option[gitdomain.Remote], runner subshelldomain.Runner) error {
 	if valueAlreadyInGit.Equal(valueToWriteToGit) {
 		return nil
diff --git a/internal/skip/execute.go b/internal/skip/execute.go
index d2d0168b6..890cc4069 100644
--- a/internal/skip/execute.go
+++ b/internal/skip/execute.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/git-town/git-town/v21/internal/cli/dialog/dialogcomponents"
 	"github.com/git-town/git-town/v21/internal/config"
-	"github.com/git-town/git-town/v21/internal/config/configdomain"
 	"github.com/git-town/git-town/v21/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v21/internal/git"
 	"github.com/git-town/git-town/v21/internal/git/gitdomain"
@@ -28,7 +27,6 @@ type ExecuteArgs struct {
 	CommandsCounter Mutable[gohacks.Counter]
 	Config          config.ValidatedConfig
 	Connector       Option[forgedomain.Connector]
-	Detached        configdomain.Detached
 	FinalMessages   stringslice.Collector
 	Frontend        subshelldomain.Runner
 	Git             git.Commands
@@ -45,7 +43,6 @@ func Execute(args ExecuteArgs) error {
 		Backend:       args.Backend,
 		Config:        args.Config,
 		Connector:     args.Connector,
-		Detached:      args.Detached,
 		FinalMessages: args.FinalMessages,
 		Frontend:      args.Frontend,
 		Git:           args.Git,
@@ -61,7 +58,6 @@ func Execute(args ExecuteArgs) error {
 		CommandsCounter:         args.CommandsCounter,
 		Config:                  args.Config,
 		Connector:               args.Connector,
-		Detached:                args.Detached,
 		FinalMessages:           args.FinalMessages,
 		Frontend:                args.Frontend,
 		Git:                     args.Git,
@@ -111,7 +107,6 @@ func revertChangesToCurrentBranch(args ExecuteArgs) error {
 	undoCurrentBranchProgram := spans.Changes().UndoProgram(undobranches.BranchChangesUndoProgramArgs{
 		BeginBranch:              args.InitialBranch,
 		Config:                   args.Config,
-		Detached:                 args.Detached,
 		EndBranch:                args.InitialBranch,
 		FinalMessages:            args.FinalMessages,
 		UndoAPIProgram:           args.RunState.UndoAPIProgram,
@@ -121,7 +116,6 @@ func revertChangesToCurrentBranch(args ExecuteArgs) error {
 		Backend:       args.Backend,
 		Config:        args.Config,
 		Connector:     args.Connector,
-		Detached:      args.Detached,
 		FinalMessages: args.FinalMessages,
 		Frontend:      args.Frontend,
 		Git:           args.Git,
diff --git a/internal/undo/execute.go b/internal/undo/execute.go
index 811ed7c4e..d7e135e34 100644
--- a/internal/undo/execute.go
+++ b/internal/undo/execute.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/git-town/git-town/v21/internal/cli/print"
 	"github.com/git-town/git-town/v21/internal/config"
-	"github.com/git-town/git-town/v21/internal/config/configdomain"
 	"github.com/git-town/git-town/v21/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v21/internal/git"
 	"github.com/git-town/git-town/v21/internal/git/gitdomain"
@@ -24,7 +23,6 @@ type ExecuteArgs struct {
 	CommandsCounter  Mutable[gohacks.Counter]
 	Config           config.ValidatedConfig
 	Connector        Option[forgedomain.Connector]
-	Detached         configdomain.Detached
 	FinalMessages    stringslice.Collector
 	Frontend         subshelldomain.Runner
 	Git              git.Commands
@@ -53,7 +51,6 @@ func Execute(args ExecuteArgs) error {
 		Backend:       args.Backend,
 		Config:        args.Config,
 		Connector:     args.Connector,
-		Detached:      args.Detached,
 		FinalMessages: args.FinalMessages,
 		Frontend:      args.Frontend,
 		Git:           args.Git,
diff --git a/internal/undo/undobranches/branch_changes.go b/internal/undo/undobranches/branch_changes.go
index 19f09e43c..93c13702f 100644
--- a/internal/undo/undobranches/branch_changes.go
+++ b/internal/undo/undobranches/branch_changes.go
@@ -5,7 +5,6 @@ import (
 	"slices"
 
 	"github.com/git-town/git-town/v21/internal/config"
-	"github.com/git-town/git-town/v21/internal/config/configdomain"
 	"github.com/git-town/git-town/v21/internal/git/gitdomain"
 	"github.com/git-town/git-town/v21/internal/gohacks/stringslice"
 	"github.com/git-town/git-town/v21/internal/messages"
@@ -201,7 +200,6 @@ func (self BranchChanges) UndoProgram(args BranchChangesUndoProgramArgs) program
 type BranchChangesUndoProgramArgs struct {
 	BeginBranch              gitdomain.LocalBranchName
 	Config                   config.ValidatedConfig
-	Detached                 configdomain.Detached
 	EndBranch                gitdomain.LocalBranchName
 	FinalMessages            stringslice.Collector
 	UndoAPIProgram           program.Program
diff --git a/internal/undo/undobranches/undo_program.go b/internal/undo/undobranches/undo_program.go
index 9c5f112e4..c7f4cb495 100644
--- a/internal/undo/undobranches/undo_program.go
+++ b/internal/undo/undobranches/undo_program.go
@@ -14,7 +14,6 @@ func DetermineUndoBranchesProgram(beginBranchesSnapshot, endBranchesSnapshot git
 	return branchChanges.UndoProgram(BranchChangesUndoProgramArgs{
 		BeginBranch:              beginBranchesSnapshot.Active.GetOrDefault(),
 		Config:                   validatedConfig,
-		Detached:                 true,
 		EndBranch:                endBranchesSnapshot.Active.GetOrDefault(),
 		FinalMessages:            finalMessages,
 		UndoAPIProgram:           undoAPIProgram,
diff --git a/internal/validate/handle_unfinished_state.go b/internal/validate/handle_unfinished_state.go
index 5a7fda17f..b743b4308 100644
--- a/internal/validate/handle_unfinished_state.go
+++ b/internal/validate/handle_unfinished_state.go
@@ -90,7 +90,6 @@ type UnfinishedStateArgs struct {
 	Backend           subshelldomain.RunnerQuerier
 	CommandsCounter   Mutable[gohacks.Counter]
 	Connector         Option[forgedomain.Connector]
-	Detached          configdomain.Detached
 	FinalMessages     stringslice.Collector
 	Frontend          subshelldomain.Runner
 	Git               git.Commands
@@ -121,7 +120,6 @@ func continueRunstate(runState runstate.RunState, args UnfinishedStateArgs) (dia
 		CommandsCounter:         args.CommandsCounter,
 		Config:                  validatedConfig,
 		Connector:               args.Connector,
-		Detached:                args.Detached,
 		FinalMessages:           args.FinalMessages,
 		Frontend:                args.Frontend,
 		Git:                     args.Git,
@@ -201,7 +199,6 @@ func skipRunstate(args UnfinishedStateArgs, runState runstate.RunState) (dialogd
 		CommandsCounter: args.CommandsCounter,
 		Config:          validatedConfig,
 		Connector:       args.Connector,
-		Detached:        args.Detached,
 		FinalMessages:   args.FinalMessages,
 		Frontend:        args.Frontend,
 		Git:             args.Git,
@@ -228,7 +225,6 @@ func undoRunState(args UnfinishedStateArgs, runState runstate.RunState) (dialogd
 		CommandsCounter:  args.CommandsCounter,
 		Config:           validatedConfig,
 		Connector:        args.Connector,
-		Detached:         args.Detached,
 		FinalMessages:    args.FinalMessages,
 		Frontend:         args.Frontend,
 		Git:              args.Git,
diff --git a/internal/vm/interpreter/fullinterpreter/auto_undo.go b/internal/vm/interpreter/fullinterpreter/auto_undo.go
index d540f3edb..1e05392cc 100644
--- a/internal/vm/interpreter/fullinterpreter/auto_undo.go
+++ b/internal/vm/interpreter/fullinterpreter/auto_undo.go
@@ -33,7 +33,6 @@ func autoUndo(opcode shared.Opcode, runErr error, args ExecuteArgs) error {
 		Backend:       args.Backend,
 		Config:        args.Config,
 		Connector:     args.Connector,
-		Detached:      args.Detached,
 		FinalMessages: args.FinalMessages,
 		Frontend:      args.Frontend,
 		Git:           args.Git,
diff --git a/internal/vm/interpreter/fullinterpreter/execute.go b/internal/vm/interpreter/fullinterpreter/execute.go
index 2f3186bd8..92c7db23f 100644
--- a/internal/vm/interpreter/fullinterpreter/execute.go
+++ b/internal/vm/interpreter/fullinterpreter/execute.go
@@ -22,7 +22,6 @@ type ExecuteArgs struct {
 	CommandsCounter         Mutable[gohacks.Counter]
 	Config                  config.ValidatedConfig
 	Connector               Option[forgedomain.Connector]
-	Detached                configdomain.Detached
 	FinalMessages           stringslice.Collector
 	Frontend                subshelldomain.Runner
 	Git                     git.Commands
@@ -64,7 +63,6 @@ func Execute(args ExecuteArgs) error {
 			BranchInfos:                     Some(args.InitialBranchesSnapshot.Branches),
 			Config:                          NewMutable(&args.Config),
 			Connector:                       args.Connector,
-			Detached:                        args.Detached,
 			FinalMessages:                   args.FinalMessages,
 			Frontend:                        args.Frontend,
 			Git:                             args.Git,
diff --git a/internal/vm/interpreter/lightinterpreter/execute.go b/internal/vm/interpreter/lightinterpreter/execute.go
index 886b9ea36..f2fdfe6cf 100644
--- a/internal/vm/interpreter/lightinterpreter/execute.go
+++ b/internal/vm/interpreter/lightinterpreter/execute.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/git-town/git-town/v21/internal/cli/dialog/dialogcomponents"
 	"github.com/git-town/git-town/v21/internal/config"
-	"github.com/git-town/git-town/v21/internal/config/configdomain"
 	"github.com/git-town/git-town/v21/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v21/internal/git"
 	"github.com/git-town/git-town/v21/internal/git/gitdomain"
@@ -28,7 +27,6 @@ func Execute(args ExecuteArgs) {
 			BranchInfos:                     None[gitdomain.BranchInfos](),
 			Config:                          NewMutable(&args.Config),
 			Connector:                       args.Connector,
-			Detached:                        args.Detached,
 			FinalMessages:                   args.FinalMessages,
 			Frontend:                        args.Frontend,
 			Git:                             args.Git,
@@ -47,7 +45,6 @@ type ExecuteArgs struct {
 	Backend       subshelldomain.RunnerQuerier
 	Config        config.ValidatedConfig
 	Connector     Option[forgedomain.Connector]
-	Detached      configdomain.Detached
 	FinalMessages stringslice.Collector
 	Frontend      subshelldomain.Runner
 	Git           git.Commands
diff --git a/internal/vm/opcodes/rebase_ancestors_until_local.go b/internal/vm/opcodes/rebase_ancestors_until_local.go
index c07513eb7..8547347e2 100644
--- a/internal/vm/opcodes/rebase_ancestors_until_local.go
+++ b/internal/vm/opcodes/rebase_ancestors_until_local.go
@@ -28,7 +28,7 @@ func (self *RebaseAncestorsUntilLocal) Run(args shared.RunArgs) error {
 			break
 		}
 		ancestorIsPerennial := args.Config.Value.IsMainOrPerennialBranch(ancestor)
-		if ancestorIsPerennial && args.Detached.IsTrue() {
+		if ancestorIsPerennial && args.Config.Value.NormalConfig.Detached.IsTrue() {
 			break
 		}
 		ancestorIsLocal := branchInfos.HasLocalBranch(ancestor)
diff --git a/internal/vm/opcodes/sync_feature_branch_compress.go b/internal/vm/opcodes/sync_feature_branch_compress.go
index 0eccf6211..fb17637a2 100644
--- a/internal/vm/opcodes/sync_feature_branch_compress.go
+++ b/internal/vm/opcodes/sync_feature_branch_compress.go
@@ -29,7 +29,7 @@ func (self *SyncFeatureBranchCompress) Run(args shared.RunArgs) error {
 			return err
 		}
 		parentIsPerennial := args.Config.Value.IsMainOrPerennialBranch(parentLocalName)
-		skipParent := args.Detached.IsTrue() && parentIsPerennial
+		skipParent := args.Config.Value.NormalConfig.Detached.IsTrue() && parentIsPerennial
 		if !inSyncWithParent && !skipParent {
 			opcodes = append(opcodes, &SyncFeatureBranchMerge{
 				Branch:            self.CurrentBranch,
diff --git a/internal/vm/opcodes/sync_feature_branch_merge.go b/internal/vm/opcodes/sync_feature_branch_merge.go
index f9c67f158..85b45f0ad 100644
--- a/internal/vm/opcodes/sync_feature_branch_merge.go
+++ b/internal/vm/opcodes/sync_feature_branch_merge.go
@@ -29,7 +29,7 @@ func (self *SyncFeatureBranchMerge) Run(args shared.RunArgs) error {
 			break
 		}
 		parentIsPerennial := args.Config.Value.IsMainOrPerennialBranch(parent)
-		if args.Detached.IsTrue() && parentIsPerennial {
+		if args.Config.Value.NormalConfig.Detached.IsTrue() && parentIsPerennial {
 			break
 		}
 		if parentBranchInfo, hasParentInfo := branchInfos.FindLocalOrRemote(parent, args.Config.Value.NormalConfig.DevRemote).Get(); hasParentInfo {
diff --git a/internal/vm/shared/runargs.go b/internal/vm/shared/runargs.go
index ba084f439..ad3dacef7 100644
--- a/internal/vm/shared/runargs.go
+++ b/internal/vm/shared/runargs.go
@@ -3,7 +3,6 @@ package shared
 import (
 	"github.com/git-town/git-town/v21/internal/cli/dialog/dialogcomponents"
 	"github.com/git-town/git-town/v21/internal/config"
-	"github.com/git-town/git-town/v21/internal/config/configdomain"
 	"github.com/git-town/git-town/v21/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v21/internal/git"
 	"github.com/git-town/git-town/v21/internal/git/gitdomain"
@@ -17,7 +16,6 @@ type RunArgs struct {
 	BranchInfos                     Option[gitdomain.BranchInfos]
 	Config                          Mutable[config.ValidatedConfig]
 	Connector                       Option[forgedomain.Connector]
-	Detached                        configdomain.Detached
 	FinalMessages                   stringslice.Collector
 	Frontend                        subshelldomain.Runner
 	Git                             git.Commands
diff --git a/website/src/SUMMARY.md b/website/src/SUMMARY.md
index b819253d5..7d2a71630 100644
--- a/website/src/SUMMARY.md
+++ b/website/src/SUMMARY.md
@@ -85,6 +85,7 @@
     - [Ship strategy](preferences/ship-strategy.md)
   - [Sync]()
     - [Auto-resolve phantom conflicts](preferences/auto-resolve.md)
+    - [Detached](preferences/detached.md)
     - [Feature sync strategy](preferences/sync-feature-strategy.md)
     - [Perennial sync strategy](preferences/sync-perennial-strategy.md)
     - [Prototype sync strategy](preferences/sync-prototype-strategy.md)
diff --git a/website/src/commands/append.md b/website/src/commands/append.md
index a2a87fd94..c129f5ec5 100644
--- a/website/src/commands/append.md
+++ b/website/src/commands/append.md
@@ -45,11 +45,16 @@ branch with the main branch as its parent.
 Adding the `--prototype` aka `-p` switch creates a
 [prototype branch](../branch-types.md#prototype-branches).
 
-#### `-d`<br>`--detached`
+#### `-d`<br>`--detached`<br>`--no-detached`
 
-The `--detached` aka `-d` flag does not pull updates from the main or perennial
-branch. This allows you to build out your stack and decide when to pull in
-changes from other developers.
+The `--detached` aka `-d` flag enables
+[detached mode](../preferences/detached.md) for the current command. If detached
+mode is enabled through [configuration data](../preferences/detached.md), the
+`--no-detached` flag disables detached mode for the current command.
+
+In detached mode, feature branches don't receive updates from the perennial
+branch at the root of your branch hierarchy. This can be useful in busy
+monorepos.
 
 #### `-c`<br>`--commit`
 
diff --git a/website/src/commands/hack.md b/website/src/commands/hack.md
index 0729f1002..a1457ff63 100644
--- a/website/src/commands/hack.md
+++ b/website/src/commands/hack.md
@@ -61,11 +61,16 @@ observed, parked, or prototype branch into a feature branch.
 Adding the `--prototype` aka `-p` switch creates a
 [prototype branch](../branch-types.md#prototype-branches).
 
-#### `-d`<br>`--detached`
+#### `-d`<br>`--detached`<br>`--no-detached`
 
-The `--detached` aka `-d` flag does not pull updates from the main or perennial
-branch. This allows you to build out your stack and decide when to pull in
-changes from other developers.
+The `--detached` aka `-d` flag enables
+[detached mode](../preferences/detached.md) for the current command. If detached
+mode is enabled through [configuration data](../preferences/detached.md), the
+`--no-detached` flag disables detached mode for the current command.
+
+In detached mode, feature branches don't receive updates from the perennial
+branch at the root of your branch hierarchy. This can be useful in busy
+monorepos.
 
 #### `-c`<br>`--commit`
 
diff --git a/website/src/commands/prepend.md b/website/src/commands/prepend.md
index a971b400e..03dbd646a 100644
--- a/website/src/commands/prepend.md
+++ b/website/src/commands/prepend.md
@@ -60,11 +60,16 @@ Adding the `--prototype` aka `-p` switch creates a
 Pre-populate the title of the pull request to create with the given text.
 Requires `--propose`.
 
-#### `-d`<br>`--detached`
+#### `-d`<br>`--detached`<br>`--no-detached`
 
-The `--detached` aka `-d` flag does not pull updates from the main or perennial
-branch. This allows you to build out your stack and decide when to pull in
-changes from other developers.
+The `--detached` aka `-d` flag enables
+[detached mode](../preferences/detached.md) for the current command. If detached
+mode is enabled through [configuration data](../preferences/detached.md), the
+`--no-detached` flag disables detached mode for the current command.
+
+In detached mode, feature branches don't receive updates from the perennial
+branch at the root of your branch hierarchy. This can be useful in busy
+monorepos.
 
 #### `-c`<br>`--commit`
 
diff --git a/website/src/commands/propose.md b/website/src/commands/propose.md
index c27c9f074..04c2c9882 100644
--- a/website/src/commands/propose.md
+++ b/website/src/commands/propose.md
@@ -8,7 +8,7 @@ The _propose_ command helps create a new pull request (also known as merge
 request) for the current feature branch. It opens your forge's website to create
 a new proposal in your browser and pre-populates information like branch and
 source/target repository. It also [syncs](sync.md) the branch to merge before
-opening the pull request in [detached](sync.md#-d--detached) mode.
+opening the pull request in [detached](sync.md#-d--detached--no-detached) mode.
 
 Proposing prototype and parked branches makes them feature branches.
 
diff --git a/website/src/commands/sync.md b/website/src/commands/sync.md
index 523de1841..d9843159c 100644
--- a/website/src/commands/sync.md
+++ b/website/src/commands/sync.md
@@ -35,7 +35,7 @@ without losing changes, even in edge cases.
 If you don't sync because:
 
 - you don't want to pull in new changes from the main branch:
-  [sync detached](sync.md#-d--detached).
+  [sync detached](sync.md#-d--detached--no-detached).
 - you don't want to increase pressure on your CI server:
   [sync without pushing](sync.md#--no-push) or indicate in your commit messages
   to CI to skip test runs
@@ -84,12 +84,16 @@ branch.
 The `--stack` aka `-s` parameter makes Git Town sync all branches in the stack
 that the current branch belongs to.
 
-#### `-d`<br>`--detached`
+#### `-d`<br>`--detached`<br>`--no-detached`
 
-The `--detached` aka `-d` flag does not pull updates from the main or perennial
-branch at the root of your branch hierarchy. This allows you to keep your
-branches in sync with each other and decide when to pull in changes from other
-developers.
+The `--detached` aka `-d` flag enables
+[detached mode](../preferences/detached.md) for the current command. If detached
+mode is enabled through [configuration data](../preferences/detached.md), the
+`--no-detached` flag disables detached mode for the current command.
+
+In detached mode, feature branches don't receive updates from the perennial
+branch at the root of your branch hierarchy. This can be useful in busy
+monorepos.
 
 #### `-p`<br>`--prune`
 
diff --git a/website/src/how-to/limit-pulling-new-changes.md b/website/src/how-to/limit-pulling-new-changes.md
index a1234c668..5eddbfb2d 100644
--- a/website/src/how-to/limit-pulling-new-changes.md
+++ b/website/src/how-to/limit-pulling-new-changes.md
@@ -6,5 +6,5 @@ you might want to avoid pulling the latest changes—like when you're reorganizi
 your stack or trying to dodge costly rebuilds triggered by upstream updates.
 
 In those cases, use
-[git town sync --detached](../commands/sync.md#-d--detached). It syncs your
-stack without touching the main branch.
+[git town sync --detached](../commands/sync.md#-d--detached--no-detached). It
+syncs your stack without touching the main branch.
diff --git a/website/src/preferences/detached.md b/website/src/preferences/detached.md
new file mode 100644
index 000000000..861b663c1
--- /dev/null
+++ b/website/src/preferences/detached.md
@@ -0,0 +1,45 @@
+# Detached
+
+This setting configures whether Git Town pulls new commits from the main or
+perennial branch at the root of your branch hierarchy. This can help if you
+encounter too many interruptions through expensive recompiles in busy monorepos
+after syncing.
+
+## options
+
+When set to `false` (the default value), `git town sync` pulls updates from the
+perennial root branch of your stack. When set to `true`, `git town sync` does
+not pull in changes from the perennial root.
+
+## via CLI flag
+
+Commands that sync branches have a `--detached` CLI flag to enable detached mode
+for that invocation. If detached mode is enabled permanently via the
+configuration settings described on this page, you can override it for a single
+run by with the `--no-detached` flag.
+
+## in config file
+
+The [config file](../configuration-file.md) can enable detached mode permanently
+for all commands like this:
+
+```toml
+[sync]
+detached = true
+```
+
+## in Git metadata
+
+To enable detached mode permanently for all commands using Git metadata:
+
+```wrap
+git config [--global] git-town.detached <true|false>
+```
+
+The optional `--global` flag applies this setting to all Git repositories on
+your machine. Without it, the setting applies only to the current repository.
+
+## environment variable
+
+You can configure whether Git Town syncs Git tags by setting the
+`GIT_TOWN_DETACHED` environment variable.
diff --git a/website/src/stacked-changes.md b/website/src/stacked-changes.md
index 822ee9d4e..f46c72029 100644
--- a/website/src/stacked-changes.md
+++ b/website/src/stacked-changes.md
@@ -266,7 +266,7 @@ Here are some best practices to minimize phantom merge conflicts:
    especially when they touch the same files.
 
    If you are hesitant to sync because it takes too long, use the
-   [--detached](commands/sync.md#-d--detached) and
+   [--detached](commands/sync.md#-d--detached--no-detached) and
    [--no-push](commands/sync.md#--no-push) flags to speed it up.
 
 2. Enable Git's [rerere](https://git-scm.com/book/en/v2/Git-Tools-Rerere)



Test patch:
diff --git a/internal/config/configfile/load_test.go b/internal/config/configfile/load_test.go
index 41ed1e29e..e07d74418 100644
--- a/internal/config/configfile/load_test.go
+++ b/internal/config/configfile/load_test.go
@@ -52,6 +52,7 @@ strategy = "api"
 
 [sync]
 auto-resolve = false
+detached = true
 feature-strategy = "merge"
 perennial-strategy = "rebase"
 prototype-strategy = "compress"
@@ -95,6 +96,7 @@ prototype-branches = "compress"
 				},
 				Sync: &configfile.Sync{
 					AutoResolve:       Ptr(false),
+					Detached:          Ptr(true),
 					FeatureStrategy:   Ptr("merge"),
 					PerennialStrategy: Ptr("rebase"),
 					PrototypeStrategy: Ptr("compress"),