Repository: git-town/git-town

Issue:
Option to prepend username to branch
In our company, we have branch naming conventions where we prepend the branch name with our username. Such as `james-harlyy/feature-a`. It would be nice to have an option in the Git Town Config to automatically do this when creating branches via `hack`, `append`, `prepend`, `rename` etc.

I'm opening a conversation to start this, I can potentially contribute this myself.

Hints from issue comments:
@kevgo: Thanks for reaching out about this, I like the idea! Can you please share some thoughts how this should work in your opinion? Here are some ideas:

- set a (hypothetical) Git Town setting `branch-prefix` to `james-harlyy/`
- `git town hack feature-a` ==> Git Town creates branch `james-harlyy/feature-a`
- `git town hack james-harlyy/feature-a` ==> Git Town recognizes that the prefix already exists and does not prepend another one
- `git town rename feature-b` ==> Git Town renames the current branch to `james-harlyy/feature-b`

If I want to create a branch without the configured prefix, I would override the configured prefix using a CLI flag:

```sh
git town hack --branch-prefix="" release/2025_10
```

Or set a different prefix for that call:

```
git town hack --branch-prefix="release/" 2025_10
```

Or do the same via an environment variable:

```
GIT_TOWN_BRANCH_PREFIX=release/ git town hack 2025_10
```@kevgo: If you’d like to take a stab at implementing this, that would be awesome! Feel free to take on as much or as little of it as you like.

Here's a minimal starting point: loading the setting from Git metadata and using it in the `append` command.

#### Set up the configuration setting:

* Create a new data type for this setting, similar to [GitHubToken](https://github.com/git-town/git-town/issues/internal/forge/forgedomain/github_token.go).
* Add a field for it to [PartialConfig](https://github.com/git-town/git-town/blob/main/internal/config/configdomain/partial_config.go) and [NormalConfig](https://github.com/git-town/git-town/blob/main/internal/config/normal_config.go).
* Extract its value from the bulk-loaded Git metadata in [NewPartialConfigFromSnapshot](https://github.com/git-town/git-town/blob/main/internal/config/parse_snapshot.go).

#### Use the configuration setting:

* Apply the configured prefix to `targetBranch` in [append](https://github.com/git-town/git-town/blob/main/internal/cmd/append.go).

#### Development tips:

* `make install` runs the type checker and installs the binary into `~/go/bin` for local testing.
* `make lint` runs the linters.
* `make unit` runs the unit tests.

At every step, the linters will guide you toward what's missing.

Don't hesitate to reach out if you get stuck or have questions, no matter how big or small. Happy hacking! 🚀
@james-harlyy: Thanks @kevgo , I'll give this a blast over the next week. Looking forward to working with this codebase.

Specifically for the git town config file, it won't be the case of specifying e.g., "james-harlyy", since the idea would be it uses `gh api user --jq '.login'` (or something similar for other connectors) in order to get the username... I like the idea of the different flags to change the prefix behaviour as well.@stephenwade: I think the prefix should be customizable. This will make it usable by people who want a prefix with a different pattern (maybe a shortened version of their username, or hyphen instead of slash, etc.).

For example, maybe my username is stephenwade-corp, but I want my branch prefix to be "stephenwade/" or "sw-".@kevgo: Hard-coding a script like `gh api user --jq '.login'` in the Git Town config file automatically sets the branch prefix for every team member out of the box. While this is nicely automated and convenient, I anticipate a few challenges: 

1. `gh api user` makes a roundtrip to the GitHub API. This takes time (sometimes a lot of time) and would run for every Git Town command that creates branches. 

2. This assumes that every person who works on this codebase uses `gh` and has it configured as the Forge connector. Even if this is true today, it might not be true tomorrow for external contractors, external auditors, etc. 

I think one of the best ways to use scripts like `gh api user` for reading config data is running the script once at login or first use, and caching the result in an environment variable like GIT_TOWN_BRANCH_PREFIX. Git Town would use this env variable once this ticket is implemented.

Another advantage of using environment variables for this is that it works well with [direnv](https://github.com/direnv/direnv).

A downside is that every person on the team would need to configure this environment variable on their own. 


Solution patch:
diff --git a/cucumber-sort.json b/cucumber-sort.json
index 1e1541373..a04895658 100644
--- a/cucumber-sort.json
+++ b/cucumber-sort.json
@@ -3,10 +3,14 @@
     "features/**/*.feature"
   ],
   "exclude": [
+    "features/append/branch_prefix/config_file.feature",
     "features/detach/merge_conflict/dependent_changes.feature",
     "features/end_to_end/ship_prototype_branch_and_sync.feature",
+    "features/hack/branch_prefix/config_file.feature",
     "features/observe/give_existing_branch/multiple_branches.feature",
     "features/prepend/beam/stack/dependent_changes.feature",
+    "features/prepend/branch_prefix/config_file.feature",
+    "features/rename/branch_prefix/config_file.feature",
     "features/set_parent/hoist/rebase/dependent_changes.feature",
     "features/ship/always_merge/current_branch/additional_commits/before_undo/online.feature",
     "features/ship/always_merge/current_branch/ship_delete_tracking_branch/disabled.feature",
diff --git a/features/append/branch_prefix/config_file.feature b/features/append/branch_prefix/config_file.feature
new file mode 100644
index 000000000..ed21ad231
--- /dev/null
+++ b/features/append/branch_prefix/config_file.feature
@@ -0,0 +1,46 @@
+Feature: append with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town append <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+          kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town append feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/append/branch_prefix/env_var.feature b/features/append/branch_prefix/env_var.feature
new file mode 100644
index 000000000..271a6df96
--- /dev/null
+++ b/features/append/branch_prefix/env_var.feature
@@ -0,0 +1,43 @@
+Feature: append with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town append <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+          kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town append feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/append/branch_prefix/git_metadata.feature b/features/append/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..2f80749b1
--- /dev/null
+++ b/features/append/branch_prefix/git_metadata.feature
@@ -0,0 +1,42 @@
+Feature: append with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town append <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+          kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town append feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/config/color.ui/always.feature b/features/config/color.ui/always.feature
index a7ff8af25..de929910b 100644
--- a/features/config/color.ui/always.feature
+++ b/features/config/color.ui/always.feature
@@ -55,6 +55,7 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
@@ -155,6 +156,7 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: push
         stash uncommitted changes: no
@@ -274,6 +276,7 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
@@ -345,6 +348,7 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
@@ -422,6 +426,7 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
diff --git a/features/config/view/alternative_config_file.feature b/features/config/view/alternative_config_file.feature
index 32c37c960..ffdcde9ed 100644
--- a/features/config/view/alternative_config_file.feature
+++ b/features/config/view/alternative_config_file.feature
@@ -32,6 +32,7 @@ Feature: show the configuration when using an alternative config file
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
diff --git a/features/config/view/config_file.feature b/features/config/view/config_file.feature
index 6a608128c..8695102a3 100644
--- a/features/config/view/config_file.feature
+++ b/features/config/view/config_file.feature
@@ -30,6 +30,7 @@ Feature: display information from config file
       display-types = "all"
 
       [create]
+      branch-prefix = "acme-"
       share-new-branches = "push"
       stash = false
 
@@ -77,6 +78,7 @@ Feature: display information from config file
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: (not set)
         share new branches: push
         stash uncommitted changes: no
diff --git a/features/config/view/detached_head.feature b/features/config/view/detached_head.feature
index df0565987..6a4e47991 100644
--- a/features/config/view/detached_head.feature
+++ b/features/config/view/detached_head.feature
@@ -14,6 +14,7 @@ Feature: display configuration from Git metadata in detached head state
       |        | local    | commit 2 |
     And Git setting "git-town.perennial-branches" is "qa staging"
     And Git setting "git-town.auto-sync" is "false"
+    And Git setting "git-town.branch-prefix" is "acme-"
     And Git setting "git-town.display-types" is "all"
     And Git setting "git-town.perennial-regex" is "^release-"
     And Git setting "git-town.contribution-regex" is "^renovate/"
@@ -53,6 +54,7 @@ Feature: display configuration from Git metadata in detached head state
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
diff --git a/features/config/view/environment_variables.feature b/features/config/view/environment_variables.feature
index 0f3460575..61a76c84a 100644
--- a/features/config/view/environment_variables.feature
+++ b/features/config/view/environment_variables.feature
@@ -16,6 +16,7 @@ Feature: display configuration defined in environment variables
       | GIT_TOWN_AUTO_SYNC                   | false              |
       | GIT_TOWN_BITBUCKET_APP_PASSWORD      | bitbucket-password |
       | GIT_TOWN_BITBUCKET_USERNAME          | bitbucket-user     |
+      | GIT_TOWN_BRANCH_PREFIX               | acme-              |
       | GIT_TOWN_FORGEJO_TOKEN               | forgejo-token      |
       | GIT_TOWN_CONTRIBUTION_REGEX          | ^renovate/         |
       | GIT_TOWN_DETACHED                    | true               |
@@ -72,6 +73,7 @@ Feature: display configuration defined in environment variables
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: prototype
         share new branches: push
         stash uncommitted changes: no
diff --git a/features/config/view/git_metadata.feature b/features/config/view/git_metadata.feature
index b112af2c9..39506e32f 100644
--- a/features/config/view/git_metadata.feature
+++ b/features/config/view/git_metadata.feature
@@ -19,6 +19,7 @@ Feature: display configuration from Git metadata
     Given Git setting "git-town.perennial-branches" is "qa staging"
     And Git setting "git-town.perennial-regex" is "^release-"
     And Git setting "git-town.auto-sync" is "false"
+    And Git setting "git-town.branch-prefix" is "acme-"
     And Git setting "git-town.contribution-regex" is "^renovate/"
     And Git setting "git-town.display-types" is "all"
     And Git setting "git-town.observed-regex" is "^dependabot/"
@@ -53,6 +54,7 @@ Feature: display configuration from Git metadata
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
@@ -123,6 +125,7 @@ Feature: display configuration from Git metadata
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
diff --git a/features/config/view/in_subfolder.feature b/features/config/view/in_subfolder.feature
index 8bde003ed..3b561e52b 100644
--- a/features/config/view/in_subfolder.feature
+++ b/features/config/view/in_subfolder.feature
@@ -33,6 +33,7 @@ Feature: show the configuration from a subfolder
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
diff --git a/features/config/view/invalid_git_metadata.feature b/features/config/view/invalid_git_metadata.feature
index e0eaa4faa..70751f305 100644
--- a/features/config/view/invalid_git_metadata.feature
+++ b/features/config/view/invalid_git_metadata.feature
@@ -2,6 +2,7 @@ Feature: display invalid Git metadata
 
   Scenario: invalid sync-feature-strategy
     Given a Git repo with origin
+    And local Git setting "git-town.branch-prefix" is ""
     And local Git setting "git-town.feature-regex" is "(feat"
     And local Git setting "git-town.perennial-regex" is "(per"
     And local Git setting "git-town.proposals-show-lineage" is "zonk"
@@ -69,6 +70,7 @@ Feature: display invalid Git metadata
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
diff --git a/features/config/view/lang/es.feature b/features/config/view/lang/es.feature
index 8dc2cfac7..9f21868af 100644
--- a/features/config/view/lang/es.feature
+++ b/features/config/view/lang/es.feature
@@ -48,6 +48,7 @@ Feature: show the configuration in Spanish
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
diff --git a/features/config/view/multiple_config_sources.feature b/features/config/view/multiple_config_sources.feature
index 9e90d8716..1980d8474 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.auto-sync" is "false"
+    And Git setting "git-town.branch-prefix" is "git-"
     And Git setting "git-town.contribution-regex" is "^git-contribution-regex"
     And Git setting "git-town.detached" is "true"
     And Git setting "git-town.display-types" is "no"
@@ -53,6 +54,7 @@ Feature: show the configuration
       display-types = "all"
 
       [create]
+      branch-prefix = "file-"
       share-new-branches = "push"
       stash = true
 
@@ -98,6 +100,7 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: git-
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
diff --git a/features/config/view/no_config.feature b/features/config/view/no_config.feature
index c69727cfb..4691e7c48 100644
--- a/features/config/view/no_config.feature
+++ b/features/config/view/no_config.feature
@@ -35,6 +35,7 @@ Feature: no configuration data
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
diff --git a/features/hack/branch_prefix/config_file.feature b/features/hack/branch_prefix/config_file.feature
new file mode 100644
index 000000000..9391afe97
--- /dev/null
+++ b/features/hack/branch_prefix/config_file.feature
@@ -0,0 +1,47 @@
+Feature: hack with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town hack <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout main            |
+      | main      | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town hack feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/hack/branch_prefix/env_var.feature b/features/hack/branch_prefix/env_var.feature
new file mode 100644
index 000000000..6126fd2ba
--- /dev/null
+++ b/features/hack/branch_prefix/env_var.feature
@@ -0,0 +1,44 @@
+Feature: hack with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town hack <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout main            |
+      | main      | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town hack feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/hack/branch_prefix/git_metadata.feature b/features/hack/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..8a7db473a
--- /dev/null
+++ b/features/hack/branch_prefix/git_metadata.feature
@@ -0,0 +1,43 @@
+Feature: hack with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town hack <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout main            |
+      | main      | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town hack feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/init/default_values/long.feature b/features/init/default_values/long.feature
index 92ea3336f..32005b3a0 100644
--- a/features/init/default_values/long.feature
+++ b/features/init/default_values/long.feature
@@ -22,6 +22,7 @@ Feature: Accepting all default values leads to a working setup
       | feature regex               | enter      |
       | contribution regex          | enter      |
       | observed regex              | enter      |
+      | branch prefix               | enter      |
       | new branch type             | enter      |
       | unknown branch type         | enter      |
       | sync feature strategy       | enter      |
@@ -85,6 +86,7 @@ Feature: Accepting all default values leads to a working setup
     And local Git setting "git-town.forge-type" still doesn't exist
     And local Git setting "git-town.github-token" still doesn't exist
     And local Git setting "git-town.hosting-origin-hostname" still doesn't exist
+    And local Git setting "git-town.branch-prefix" still doesn't exist
     And local Git setting "git-town.proposals-show-lineage" still doesn't exist
     And local Git setting "git-town.feature-regex" now doesn't exist
     And local Git setting "git-town.contribution-regex" now doesn't exist
diff --git a/features/init/existing_configuration/change_git_metadata.feature b/features/init/existing_configuration/change_git_metadata.feature
index 64246db85..678b0a88a 100644
--- a/features/init/existing_configuration/change_git_metadata.feature
+++ b/features/init/existing_configuration/change_git_metadata.feature
@@ -33,6 +33,7 @@ Feature: change existing information in Git metadata
       | feature regex               | f e a t enter          |
       | contribution regex          | c o n t enter          |
       | observed regex              | o b s enter            |
+      | branch prefix               | k g - enter            |
       | new branch type             | down enter             |
       | unknown branch type         | down enter             |
       | sync feature strategy       | down enter             |
@@ -78,6 +79,7 @@ Feature: change existing information in Git metadata
       | git config git-town.forge-type github                    |
       | git config git-town.github-connector api                 |
       | git config git-town.auto-sync true                       |
+      | git config git-town.branch-prefix kg-                    |
       | git config git-town.detached true                        |
       | git config git-town.new-branch-type prototype            |
       | git config git-town.perennial-regex per                  |
diff --git a/features/init/existing_configuration/config_file_complete.feature b/features/init/existing_configuration/config_file_complete.feature
index 6c88ccfa3..32c7c9213 100644
--- a/features/init/existing_configuration/config_file_complete.feature
+++ b/features/init/existing_configuration/config_file_complete.feature
@@ -16,6 +16,7 @@ Feature: don't ask for information already provided by the config file
       order = "desc"
 
       [create]
+      branch-prefix = "acme-"
       new-branch-type = "feature"
       share-new-branches = "propose"
       stash = true
diff --git a/features/init/existing_configuration/config_file_partial.feature b/features/init/existing_configuration/config_file_partial.feature
index 608a9371e..0a3caa4e7 100644
--- a/features/init/existing_configuration/config_file_partial.feature
+++ b/features/init/existing_configuration/config_file_partial.feature
@@ -40,6 +40,7 @@ Feature: ask for information not provided by the config file
       | feature regex           | f e a t enter         |
       | contribution regex      | c o n t enter         |
       | observed regex          | o b s enter           |
+      | branch prefix           | a c m e - enter       |
       | new branch type         | enter                 |
       | unknown branch type     | enter                 |
       | sync feature strategy   | enter                 |
@@ -58,6 +59,7 @@ Feature: ask for information not provided by the config file
       | git config git-town.github-token gh-token           |
       | git config git-town.perennial-branches branch-1     |
       | git config git-town.github-connector api            |
+      | git config git-town.branch-prefix acme-             |
       | git config git-town.detached false                  |
       | git config git-town.new-branch-type feature         |
       | git config git-town.perennial-regex peren           |
diff --git a/features/init/existing_configuration/config_file_update.feature b/features/init/existing_configuration/config_file_update.feature
index 97447ca18..a96bc9dc1 100644
--- a/features/init/existing_configuration/config_file_update.feature
+++ b/features/init/existing_configuration/config_file_update.feature
@@ -77,6 +77,7 @@ Feature: don't ask for information already provided by the config file
       unknown-branch-type = "observed"
 
       [create]
+      branch-prefix = "acme-"
       new-branch-type = "feature"
       share-new-branches = "propose"
       stash = true
diff --git a/features/init/existing_configuration/git_global.feature b/features/init/existing_configuration/git_global.feature
index 585331657..fd8b9c42e 100644
--- a/features/init/existing_configuration/git_global.feature
+++ b/features/init/existing_configuration/git_global.feature
@@ -8,6 +8,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
       | branch-1 | feature | main   | local     |
     And Git Town is not configured
     And global Git setting "git-town.auto-sync" is "false"
+    And global Git setting "git-town.branch-prefix" is "kg-"
     And global Git setting "git-town.feature-regex" is "^feat-"
     And global Git setting "git-town.contribution-regex" is "^cont-"
     And global Git setting "git-town.observed-regex" is "^obs-"
@@ -45,6 +46,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
       | feature regex               | enter       |
       | contribution regex          | enter       |
       | observed regex              | enter       |
+      | branch prefix               | enter       |
       | new branch type             | enter       |
       | unknown branch type         | enter       |
       | sync feature strategy       | enter       |
diff --git a/features/init/existing_configuration/invalid_config_data.feature b/features/init/existing_configuration/invalid_config_data.feature
index c0a270743..88f9284d7 100644
--- a/features/init/existing_configuration/invalid_config_data.feature
+++ b/features/init/existing_configuration/invalid_config_data.feature
@@ -10,6 +10,7 @@ Feature: Fix invalid configuration data
     And local Git setting "git-town.contribution-regex" is "(cont"
     And local Git setting "git-town.observed-regex" is "(obs"
     And local Git setting "git-town.new-branch-type" is "zonk"
+    And local Git setting "git-town.branch-prefix" is "xx"
     And local Git setting "git-town.unknown-branch-type" is "zonk"
     And local Git setting "git-town.sync-feature-strategy" is "--help"
     And local Git setting "git-town.sync-perennial-strategy" is "zonk"
@@ -26,41 +27,43 @@ Feature: Fix invalid configuration data
     And local Git setting "git-town.ship-delete-tracking-branch" is "zonk"
     And local Git setting "git-town.order" is "zonk"
     When I run "git-town init" and enter into the dialogs:
-      | DIALOG                      | KEYS          |
-      | welcome                     | enter         |
-      | aliases                     | enter         |
-      | main branch                 | enter         |
-      | origin hostname             | enter         |
-      | forge type                  | enter         |
-      | enter all                   | down enter    |
-      | perennial regex             | p e r enter   |
-      | feature regex               | f e a t enter |
-      | contribution regex          | c o n t enter |
-      | observed regex              | o b s enter   |
-      | new branch type             | down enter    |
-      | unknown branch type         | down enter    |
-      | sync feature strategy       | down enter    |
-      | sync perennial strategy     | down enter    |
-      | sync prototype strategy     | down enter    |
-      | sync upstream               | down enter    |
-      | auto-sync                   | down enter    |
-      | sync-tags                   | down enter    |
-      | detached                    | down enter    |
-      | stash                       | down enter    |
-      | share-new-branches          | down enter    |
-      | push-branches               | down enter    |
-      | push-hook                   | down enter    |
-      | ship-strategy               | down enter    |
-      | ship-delete-tracking branch | down enter    |
-      | order                       | down enter    |
-      | proposals show lineage      | enter         |
-      | config storage              | enter         |
+      | DIALOG                      | KEYS                          |
+      | welcome                     | enter                         |
+      | aliases                     | enter                         |
+      | main branch                 | enter                         |
+      | origin hostname             | enter                         |
+      | forge type                  | enter                         |
+      | enter all                   | down enter                    |
+      | perennial regex             | p e r enter                   |
+      | feature regex               | f e a t enter                 |
+      | contribution regex          | c o n t enter                 |
+      | observed regex              | o b s enter                   |
+      | branch prefix               | backspace backspace a b enter |
+      | new branch type             | down enter                    |
+      | unknown branch type         | down enter                    |
+      | sync feature strategy       | down enter                    |
+      | sync perennial strategy     | down enter                    |
+      | sync prototype strategy     | down enter                    |
+      | sync upstream               | down enter                    |
+      | auto-sync                   | down enter                    |
+      | sync-tags                   | down enter                    |
+      | detached                    | down enter                    |
+      | stash                       | down enter                    |
+      | share-new-branches          | down enter                    |
+      | push-branches               | down enter                    |
+      | push-hook                   | down enter                    |
+      | ship-strategy               | down enter                    |
+      | ship-delete-tracking branch | down enter                    |
+      | order                       | down enter                    |
+      | proposals show lineage      | enter                         |
+      | config storage              | enter                         |
 
   Scenario: result
     Then Git Town runs the commands
       | COMMAND                                               |
       | git config git-town.main-branch main                  |
       | git config git-town.auto-sync false                   |
+      | git config git-town.branch-prefix ab                  |
       | git config git-town.detached true                     |
       | git config git-town.new-branch-type parked            |
       | git config git-town.perennial-regex per               |
diff --git a/features/init/existing_configuration/migrate_git_to_file.feature b/features/init/existing_configuration/migrate_git_to_file.feature
index bf74d0718..f0a5939ac 100644
--- a/features/init/existing_configuration/migrate_git_to_file.feature
+++ b/features/init/existing_configuration/migrate_git_to_file.feature
@@ -16,6 +16,7 @@ Feature: migrate existing configuration in Git metadata to a config file
     And local Git setting "git-town.push-hook" is "true"
     And local Git setting "git-town.new-branch-type" is "prototype"
     And local Git setting "git-town.order" is "desc"
+    And local Git setting "git-town.branch-prefix" is "acme-"
     And local Git setting "git-town.ship-strategy" is "squash-merge"
     And local Git setting "git-town.ship-delete-tracking-branch" is "false"
     And local Git setting "git-town.stash" is "false"
@@ -39,6 +40,7 @@ Feature: migrate existing configuration in Git metadata to a config file
       | feature regex               | enter      |
       | contribution regex          | enter      |
       | observed regex              | enter      |
+      | branch prefix               | enter      |
       | new branch type             | enter      |
       | unknown branch type         | enter      |
       | sync feature strategy       | enter      |
@@ -62,6 +64,7 @@ Feature: migrate existing configuration in Git metadata to a config file
     Then Git Town runs the commands
       | COMMAND                                                 |
       | git config --unset git-town.auto-sync                   |
+      | git config --unset git-town.branch-prefix               |
       | git config --unset git-town.contribution-regex          |
       | git config --unset git-town.dev-remote                  |
       | git config --unset git-town.feature-regex               |
@@ -119,6 +122,7 @@ Feature: migrate existing configuration in Git metadata to a config file
       unknown-branch-type = "observed"
 
       [create]
+      branch-prefix = "acme-"
       new-branch-type = "prototype"
       share-new-branches = "no"
       stash = false
diff --git a/features/init/existing_configuration/remove_git_metadata.feature b/features/init/existing_configuration/remove_git_metadata.feature
index 598dfc794..5b5b7ad3e 100644
--- a/features/init/existing_configuration/remove_git_metadata.feature
+++ b/features/init/existing_configuration/remove_git_metadata.feature
@@ -25,6 +25,7 @@ Feature: remove existing configuration in Git metadata
     And global Git setting "alias.ship" is "town ship"
     And global Git setting "alias.sync" is "town sync"
     And local Git setting "git-town.auto-sync" is "false"
+    And local Git setting "git-town.branch-prefix" is "kg-"
     And local Git setting "git-town.contribution-regex" is "other.*"
     And local Git setting "git-town.detached" is "true"
     And local Git setting "git-town.dev-remote" is "fork"
@@ -62,6 +63,7 @@ Feature: remove existing configuration in Git metadata
       | feature regex               | backspace backspace backspace backspace backspace backspace enter           |                     |
       | contribution regex          | backspace backspace backspace backspace backspace backspace backspace enter |                     |
       | observed regex              | backspace backspace backspace backspace backspace enter                     |                     |
+      | branch prefix               | backspace backspace backspace enter                                         |                     |
       | new branch type             | up enter                                                                    |                     |
       | unknown branch type         | up enter                                                                    |                     |
       | sync feature strategy       | up enter                                                                    |                     |
@@ -103,6 +105,7 @@ Feature: remove existing configuration in Git metadata
       | git config --unset git-town.hosting-origin-hostname  |
       | git config --unset git-town.forge-type               |
       | git config git-town.auto-sync true                   |
+      | git config --unset git-town.branch-prefix            |
       | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
       | git config --unset git-town.perennial-regex          |
@@ -174,6 +177,7 @@ Feature: remove existing configuration in Git metadata
     And global Git setting "alias.ship" is now "town ship"
     And global Git setting "alias.sync" is now "town sync"
     And local Git setting "git-town.auto-sync" is now "false"
+    And local Git setting "git-town.branch-prefix" is now "kg-"
     And local Git setting "git-town.contribution-regex" is now "other.*"
     And local Git setting "git-town.dev-remote" is now "fork"
     And local Git setting "git-town.feature-regex" is now "user.*"
diff --git a/features/prepend/branch_prefix/config_file.feature b/features/prepend/branch_prefix/config_file.feature
new file mode 100644
index 000000000..ea566bfda
--- /dev/null
+++ b/features/prepend/branch_prefix/config_file.feature
@@ -0,0 +1,46 @@
+Feature: prepend with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town prepend <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                           |
+      | feature-1 | git fetch --prune --tags          |
+      |           | git checkout -b kg-feature-2 main |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+          feature-1
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town prepend feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/prepend/branch_prefix/env_var.feature b/features/prepend/branch_prefix/env_var.feature
new file mode 100644
index 000000000..b9f8a698f
--- /dev/null
+++ b/features/prepend/branch_prefix/env_var.feature
@@ -0,0 +1,43 @@
+Feature: prepend with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town prepend <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                           |
+      | feature-1 | git fetch --prune --tags          |
+      |           | git checkout -b kg-feature-2 main |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+          feature-1
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town prepend feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/prepend/branch_prefix/git_metadata.feature b/features/prepend/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..fe37553e0
--- /dev/null
+++ b/features/prepend/branch_prefix/git_metadata.feature
@@ -0,0 +1,42 @@
+Feature: prepend with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town prepend <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                           |
+      | feature-1 | git fetch --prune --tags          |
+      |           | git checkout -b kg-feature-2 main |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+          feature-1
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town prepend feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/rename/branch_prefix/config_file.feature b/features/rename/branch_prefix/config_file.feature
new file mode 100644
index 000000000..595a21e10
--- /dev/null
+++ b/features/rename/branch_prefix/config_file.feature
@@ -0,0 +1,51 @@
+Feature: rename with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town rename <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                  |
+      | feature-1    | git fetch --prune --tags                 |
+      |              | git branch --move feature-1 kg-feature-2 |
+      |              | git checkout kg-feature-2                |
+      | kg-feature-2 | git push -u origin kg-feature-2          |
+      |              | git push origin :feature-1               |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town rename feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                                |
+      | kg-feature-2 | git branch feature-1 {{ sha 'persisted config file' }} |
+      |              | git push -u origin feature-1                           |
+      |              | git checkout feature-1                                 |
+      | feature-1    | git branch -D kg-feature-2                             |
+      |              | git push origin :kg-feature-2                          |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/rename/branch_prefix/env_var.feature b/features/rename/branch_prefix/env_var.feature
new file mode 100644
index 000000000..ab791b4ea
--- /dev/null
+++ b/features/rename/branch_prefix/env_var.feature
@@ -0,0 +1,48 @@
+Feature: rename with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town rename <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                  |
+      | feature-1    | git fetch --prune --tags                 |
+      |              | git branch --move feature-1 kg-feature-2 |
+      |              | git checkout kg-feature-2                |
+      | kg-feature-2 | git push -u origin kg-feature-2          |
+      |              | git push origin :feature-1               |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town rename feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                         |
+      | kg-feature-2 | git branch feature-1 {{ sha 'initial commit' }} |
+      |              | git push -u origin feature-1                    |
+      |              | git checkout feature-1                          |
+      | feature-1    | git branch -D kg-feature-2                      |
+      |              | git push origin :kg-feature-2                   |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/rename/branch_prefix/git_metadata.feature b/features/rename/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..d2410d35f
--- /dev/null
+++ b/features/rename/branch_prefix/git_metadata.feature
@@ -0,0 +1,47 @@
+Feature: rename with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town rename <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                  |
+      | feature-1    | git fetch --prune --tags                 |
+      |              | git branch --move feature-1 kg-feature-2 |
+      |              | git checkout kg-feature-2                |
+      | kg-feature-2 | git push -u origin kg-feature-2          |
+      |              | git push origin :feature-1               |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town rename feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                         |
+      | kg-feature-2 | git branch feature-1 {{ sha 'initial commit' }} |
+      |              | git push -u origin feature-1                    |
+      |              | git checkout feature-1                          |
+      | feature-1    | git branch -D kg-feature-2                      |
+      |              | git push origin :kg-feature-2                   |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/internal/cli/dialog/branch_prefix.go b/internal/cli/dialog/branch_prefix.go
new file mode 100644
index 000000000..d16b8dfc2
--- /dev/null
+++ b/internal/cli/dialog/branch_prefix.go
@@ -0,0 +1,46 @@
+package dialog
+
+import (
+	"cmp"
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogcomponents"
+	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogdomain"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+const (
+	branchPrefixTitle = `Branch prefix`
+	BranchPrefixHelp  = `
+When set, Git Town will automatically
+add this prefix to branches it creates or renames.
+
+For example, if set to "feature-",
+running "git town hack example"
+will create "feature-example".
+
+If you're not sure what to enter here,
+it's safe to leave it blank.
+
+`
+)
+
+func BranchPrefix(args Args[configdomain.BranchPrefix]) (Option[configdomain.BranchPrefix], dialogdomain.Exit, error) {
+	input, exit, errInput := dialogcomponents.TextField(dialogcomponents.TextFieldArgs{
+		DialogName:    "branch-prefix",
+		ExistingValue: args.Local.Or(args.Global).StringOr(""),
+		Help:          BranchPrefixHelp,
+		Inputs:        args.Inputs,
+		Prompt:        messages.BranchPrefixPrompt,
+		Title:         branchPrefixTitle,
+	})
+	newValue, errNewValue := configdomain.ParseBranchPrefix(input, "dialog")
+	if args.Global.Equal(newValue) {
+		// the user has entered the global value --> keep using the global value, don't store the local value
+		newValue = None[configdomain.BranchPrefix]()
+	}
+	fmt.Printf(messages.BranchPrefixResult, dialogcomponents.FormattedOption(newValue, args.Global.IsSome(), exit))
+	return newValue, exit, cmp.Or(errInput, errNewValue)
+}
diff --git a/internal/cmd/append.go b/internal/cmd/append.go
index e7079e99f..3747da05a 100644
--- a/internal/cmd/append.go
+++ b/internal/cmd/append.go
@@ -311,11 +311,15 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 	if err != nil {
 		return data, configdomain.ProgramFlowExit, err
 	}
-	if branchesSnapshot.Branches.HasLocalBranch(args.targetBranch) {
-		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, args.targetBranch)
+	targetBranch := args.targetBranch
+	if prefix, hasPrefix := config.BranchPrefix.Get(); hasPrefix {
+		targetBranch = prefix.Apply(targetBranch)
 	}
-	if branchesSnapshot.Branches.HasMatchingTrackingBranchFor(args.targetBranch, repo.UnvalidatedConfig.NormalConfig.DevRemote) {
-		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsRemotely, args.targetBranch)
+	if branchesSnapshot.Branches.HasLocalBranch(targetBranch) {
+		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, targetBranch)
+	}
+	if branchesSnapshot.Branches.HasMatchingTrackingBranchFor(targetBranch, repo.UnvalidatedConfig.NormalConfig.DevRemote) {
+		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsRemotely, targetBranch)
 	}
 	initialBranch, hasInitialBranch := branchesSnapshot.Active.Get()
 	if !hasInitialBranch {
@@ -362,7 +366,7 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		if err != nil {
 			return data, configdomain.ProgramFlowExit, err
 		}
-		commitsToBeam, exit, err = dialog.CommitsToBeam(commitsInBranch, args.targetBranch, repo.Git, repo.Backend, inputs)
+		commitsToBeam, exit, err = dialog.CommitsToBeam(commitsInBranch, targetBranch, repo.Git, repo.Backend, inputs)
 		if err != nil || exit {
 			return data, configdomain.ProgramFlowExit, err
 		}
@@ -393,7 +397,7 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		prototype:                 args.prototype,
 		remotes:                   remotes,
 		stashSize:                 stashSize,
-		targetBranch:              args.targetBranch,
+		targetBranch:              targetBranch,
 	}, configdomain.ProgramFlowContinue, nil
 }
 
diff --git a/internal/cmd/config/root.go b/internal/cmd/config/root.go
index 90119eaf5..74398c106 100644
--- a/internal/cmd/config/root.go
+++ b/internal/cmd/config/root.go
@@ -94,6 +94,7 @@ func printConfig(config config.UnvalidatedConfig) {
 	print.Entry("git user email", format.OptionalStringerSetting(config.NormalConfig.GitUserEmail))
 	fmt.Println()
 	print.Header("Create")
+	print.Entry("branch prefix", format.OptionalStringerSetting(config.NormalConfig.BranchPrefix))
 	print.Entry("new branch type", format.OptionalStringerSetting(config.NormalConfig.NewBranchType))
 	print.Entry("share new branches", config.NormalConfig.ShareNewBranches.String())
 	print.Entry("stash uncommitted changes", format.Bool(config.NormalConfig.Stash.ShouldStash()))
diff --git a/internal/cmd/hack.go b/internal/cmd/hack.go
index 2bbc47255..c94d69810 100644
--- a/internal/cmd/hack.go
+++ b/internal/cmd/hack.go
@@ -302,6 +302,9 @@ func determineHackData(args hackArgs, repo execute.OpenRepoResult) (data appendF
 		return data, configdomain.ProgramFlowExit, errors.New(messages.HackTooManyArguments)
 	}
 	targetBranch := targetBranches[0]
+	if prefix, hasPrefix := validatedConfig.NormalConfig.BranchPrefix.Get(); hasPrefix {
+		targetBranch = prefix.Apply(targetBranch)
+	}
 	if branchesSnapshot.Branches.HasLocalBranch(targetBranch) {
 		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, targetBranch)
 	}
diff --git a/internal/cmd/prepend.go b/internal/cmd/prepend.go
index c54c9e8d1..ad0fda581 100644
--- a/internal/cmd/prepend.go
+++ b/internal/cmd/prepend.go
@@ -324,6 +324,9 @@ func determinePrependData(args prependArgs, repo execute.OpenRepoResult) (data p
 		return data, configdomain.ProgramFlowExit, err
 	}
 	targetBranch := gitdomain.NewLocalBranchName(args.argv[0])
+	if prefix, hasPrefix := config.BranchPrefix.Get(); hasPrefix {
+		targetBranch = prefix.Apply(targetBranch)
+	}
 	if branchesSnapshot.Branches.HasLocalBranch(targetBranch) {
 		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, targetBranch)
 	}
diff --git a/internal/cmd/rename.go b/internal/cmd/rename.go
index f96ed9ac5..cbe445e88 100644
--- a/internal/cmd/rename.go
+++ b/internal/cmd/rename.go
@@ -218,6 +218,9 @@ func determineRenameData(args []string, force configdomain.Force, repo execute.O
 		oldBranchName = gitdomain.NewLocalBranchName(args[0])
 		newBranchName = gitdomain.NewLocalBranchName(args[1])
 	}
+	if prefix, hasPrefix := config.BranchPrefix.Get(); hasPrefix {
+		newBranchName = prefix.Apply(newBranchName)
+	}
 	oldBranch, hasOldBranch := branchesSnapshot.Branches.FindByLocalName(oldBranchName).Get()
 	if !hasOldBranch {
 		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchDoesntExist, oldBranchName)
diff --git a/internal/config/cliconfig/cli_config.go b/internal/config/cliconfig/cli_config.go
index 72fb3c3e7..ee5c2fb4e 100644
--- a/internal/config/cliconfig/cli_config.go
+++ b/internal/config/cliconfig/cli_config.go
@@ -26,6 +26,7 @@ func New(args NewArgs) configdomain.PartialConfig {
 		AutoSync:                 args.AutoSync,
 		BitbucketAppPassword:     None[forgedomain.BitbucketAppPassword](),
 		BitbucketUsername:        None[forgedomain.BitbucketUsername](),
+		BranchPrefix:             None[configdomain.BranchPrefix](),
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{},
 		ForgejoToken:             None[forgedomain.ForgejoToken](),
 		ContributionRegex:        None[configdomain.ContributionRegex](),
diff --git a/internal/config/configdomain/branch_prefix.go b/internal/config/configdomain/branch_prefix.go
new file mode 100644
index 000000000..b43ab0848
--- /dev/null
+++ b/internal/config/configdomain/branch_prefix.go
@@ -0,0 +1,28 @@
+package configdomain
+
+import (
+	"strings"
+
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// BranchPrefix is a prefix that gets automatically added to branches created by Git Town.
+type BranchPrefix string
+
+// Apply provides the given branch name with this prefix applied.
+func (self BranchPrefix) Apply(branch gitdomain.LocalBranchName) gitdomain.LocalBranchName {
+	if strings.HasPrefix(branch.String(), self.String()) {
+		return branch
+	}
+	return gitdomain.NewLocalBranchName(self.String() + branch.String())
+}
+
+func (self BranchPrefix) String() string { return string(self) }
+
+func ParseBranchPrefix(value, _ string) (Option[BranchPrefix], error) {
+	if value == "" {
+		return None[BranchPrefix](), nil
+	}
+	return Some(BranchPrefix(value)), nil
+}
diff --git a/internal/config/configdomain/key.go b/internal/config/configdomain/key.go
index 9d0d5b6b8..0b5b48629 100644
--- a/internal/config/configdomain/key.go
+++ b/internal/config/configdomain/key.go
@@ -58,6 +58,7 @@ const (
 	KeyAutoSync                            = Key("git-town.auto-sync")
 	KeyBitbucketAppPassword                = Key("git-town.bitbucket-app-password")
 	KeyBitbucketUsername                   = Key("git-town.bitbucket-username")
+	KeyBranchPrefix                        = Key("git-town.branch-prefix")
 	KeyContributionRegex                   = Key("git-town.contribution-regex")
 	KeyDeprecatedCodebergToken             = Key("git-town.codeberg-token")
 	KeyDeprecatedCodeHostingDriver         = Key("git-town.code-hosting-driver")
@@ -122,6 +123,7 @@ var keys = []Key{
 	KeyAutoSync,
 	KeyBitbucketAppPassword,
 	KeyBitbucketUsername,
+	KeyBranchPrefix,
 	KeyContributionRegex,
 	KeyDeprecatedAliasKill,
 	KeyDeprecatedAliasRenameBranch,
diff --git a/internal/config/configdomain/partial_config.go b/internal/config/configdomain/partial_config.go
index 82c3a8229..50d03ce33 100644
--- a/internal/config/configdomain/partial_config.go
+++ b/internal/config/configdomain/partial_config.go
@@ -21,6 +21,7 @@ type PartialConfig struct {
 	AutoSync                 Option[AutoSync]
 	BitbucketAppPassword     Option[forgedomain.BitbucketAppPassword]
 	BitbucketUsername        Option[forgedomain.BitbucketUsername]
+	BranchPrefix             Option[BranchPrefix]
 	BranchTypeOverrides      BranchTypeOverrides
 	ContributionRegex        Option[ContributionRegex]
 	Detached                 Option[Detached]
@@ -78,6 +79,7 @@ func (self PartialConfig) Merge(other PartialConfig) PartialConfig {
 		AutoSync:                 other.AutoSync.Or(self.AutoSync),
 		BitbucketAppPassword:     other.BitbucketAppPassword.Or(self.BitbucketAppPassword),
 		BitbucketUsername:        other.BitbucketUsername.Or(self.BitbucketUsername),
+		BranchPrefix:             other.BranchPrefix.Or(self.BranchPrefix),
 		BranchTypeOverrides:      other.BranchTypeOverrides.Concat(self.BranchTypeOverrides),
 		ContributionRegex:        other.ContributionRegex.Or(self.ContributionRegex),
 		Detached:                 other.Detached.Or(self.Detached),
diff --git a/internal/config/configfile/data.go b/internal/config/configfile/data.go
index 30d3d9a60..ed6fe5929 100644
--- a/internal/config/configfile/data.go
+++ b/internal/config/configfile/data.go
@@ -36,6 +36,7 @@ func (self Branches) IsEmpty() bool {
 }
 
 type Create struct {
+	BranchPrefix     *string `toml:"branch-prefix"`
 	NewBranchType    *string `toml:"new-branch-type"`
 	PushNewbranches  *bool   `toml:"push-new-branches"`
 	ShareNewBranches *string `toml:"share-new-branches"`
diff --git a/internal/config/configfile/load.go b/internal/config/configfile/load.go
index 07a2dda45..854c617f0 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
 	// keep-sorted start
 	var autoResolve Option[configdomain.AutoResolve]
 	var autoSync Option[configdomain.AutoSync]
+	var branchPrefix Option[configdomain.BranchPrefix]
 	var contributionRegex Option[configdomain.ContributionRegex]
 	var detached Option[configdomain.Detached]
 	var devRemote Option[gitdomain.Remote]
@@ -153,6 +154,10 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 		}
 	}
 	if data.Create != nil {
+		if data.Create.BranchPrefix != nil {
+			branchPrefix, err = configdomain.ParseBranchPrefix(*data.Create.BranchPrefix, messages.ConfigFile)
+			ec.Check(err)
+		}
 		if data.Create.NewBranchType != nil {
 			branchType, err := configdomain.ParseBranchType(*data.Create.NewBranchType, messages.ConfigFile)
 			ec.Check(err)
@@ -262,6 +267,7 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     None[forgedomain.BitbucketAppPassword](),
 		BitbucketUsername:        None[forgedomain.BitbucketUsername](),
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{},
 		ForgejoToken:             None[forgedomain.ForgejoToken](),
 		ContributionRegex:        contributionRegex,
diff --git a/internal/config/configfile/save.go b/internal/config/configfile/save.go
index 1d80a384b..4c60a04d1 100644
--- a/internal/config/configfile/save.go
+++ b/internal/config/configfile/save.go
@@ -75,11 +75,15 @@ func RenderTOML(data configdomain.PartialConfig) string {
 		}
 	}
 
+	branchPrefix, hasBranchPrefix := data.BranchPrefix.Get()
 	newBranchType, hasNewBranchType := data.NewBranchType.Get()
 	shareNewBranches, hasShareNewBranches := data.ShareNewBranches.Get()
 	stash, hasStash := data.Stash.Get()
-	if cmp.Or(hasNewBranchType, hasShareNewBranches, hasStash) {
+	if cmp.Or(hasBranchPrefix, hasNewBranchType, hasShareNewBranches, hasStash) {
 		result.WriteString("\n[create]\n")
+		if hasBranchPrefix {
+			result.WriteString(fmt.Sprintf("branch-prefix = %q\n", branchPrefix))
+		}
 		if hasNewBranchType {
 			result.WriteString(fmt.Sprintf("new-branch-type = %q\n", newBranchType))
 		}
diff --git a/internal/config/envconfig/load.go b/internal/config/envconfig/load.go
index 1760bc2ee..f0651cb4d 100644
--- a/internal/config/envconfig/load.go
+++ b/internal/config/envconfig/load.go
@@ -15,6 +15,7 @@ const (
 	autoSync                 = "GIT_TOWN_AUTO_SYNC"
 	bitbucketAppPassword     = "GIT_TOWN_BITBUCKET_APP_PASSWORD"
 	bitbucketUserName        = "GIT_TOWN_BITBUCKET_USERNAME"
+	branchPrefix             = "GIT_TOWN_BRANCH_PREFIX"
 	forgejoToken             = "GIT_TOWN_FORGEJO_TOKEN"
 	contributionRegex        = "GIT_TOWN_CONTRIBUTION_REGEX"
 	detached                 = "GIT_TOWN_DETACHED"
@@ -59,6 +60,7 @@ const (
 func Load(env EnvVars) (configdomain.PartialConfig, error) {
 	autoResolve, errAutoResolve := load(env, autoResolve, gohacks.ParseBoolOpt[configdomain.AutoResolve])
 	autoSync, errAutoSync := load(env, autoSync, gohacks.ParseBoolOpt[configdomain.AutoSync])
+	branchPrefix, errBranchPrefix := load(env, branchPrefix, configdomain.ParseBranchPrefix)
 	contributionRegex, errContribRegex := load(env, contributionRegex, configdomain.ParseContributionRegex)
 	detached, errDetached := load(env, detached, gohacks.ParseBoolOpt[configdomain.Detached])
 	displayTypesOpt, errDisplayTypes := load(env, displayTypes, configdomain.ParseDisplayTypes)
@@ -95,6 +97,7 @@ func Load(env EnvVars) (configdomain.PartialConfig, error) {
 	err := cmp.Or(
 		errAutoResolve,
 		errAutoSync,
+		errBranchPrefix,
 		errContribRegex,
 		errDetached,
 		errDisplayTypes,
@@ -129,6 +132,7 @@ func Load(env EnvVars) (configdomain.PartialConfig, error) {
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     forgedomain.ParseBitbucketAppPassword(env.Get(bitbucketAppPassword)),
 		BitbucketUsername:        forgedomain.ParseBitbucketUsername(env.Get(bitbucketUserName)),
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{}, // not loaded from env vars
 		ForgejoToken:             forgedomain.ParseForgejoToken(env.Get(forgejoToken)),
 		ContributionRegex:        contributionRegex,
diff --git a/internal/config/gitconfig/high_level.go b/internal/config/gitconfig/high_level.go
index 87eeece1b..ec1719335 100644
--- a/internal/config/gitconfig/high_level.go
+++ b/internal/config/gitconfig/high_level.go
@@ -49,6 +49,10 @@ func RemoveBitbucketUsername(runner subshelldomain.Runner) error {
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyBitbucketUsername)
 }
 
+func RemoveBranchPrefix(runner subshelldomain.Runner) error {
+	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyBranchPrefix)
+}
+
 func RemoveBranchTypeOverride(runner subshelldomain.Runner, branch gitdomain.LocalBranchName) error {
 	key := configdomain.NewBranchTypeOverrideKeyForBranch(branch)
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, key.Key)
@@ -198,6 +202,10 @@ func SetBitbucketUsername(runner subshelldomain.Runner, value forgedomain.Bitbuc
 	return SetConfigValue(runner, scope, configdomain.KeyBitbucketUsername, value.String())
 }
 
+func SetBranchPrefix(runner subshelldomain.Runner, value configdomain.BranchPrefix, scope configdomain.ConfigScope) error {
+	return SetConfigValue(runner, scope, configdomain.KeyBranchPrefix, value.String())
+}
+
 func SetBranchTypeOverride(runner subshelldomain.Runner, branchType configdomain.BranchType, branches ...gitdomain.LocalBranchName) error {
 	for _, branch := range branches {
 		if err := SetConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.NewBranchTypeOverrideKeyForBranch(branch).Key, branchType.String()); err != nil {
diff --git a/internal/config/normal_config.go b/internal/config/normal_config.go
index e463cedaf..989381c8f 100644
--- a/internal/config/normal_config.go
+++ b/internal/config/normal_config.go
@@ -34,6 +34,7 @@ type NormalConfig struct {
 	AutoSync                 configdomain.AutoSync
 	BitbucketAppPassword     Option[forgedomain.BitbucketAppPassword]
 	BitbucketUsername        Option[forgedomain.BitbucketUsername]
+	BranchPrefix             Option[configdomain.BranchPrefix]
 	BranchTypeOverrides      configdomain.BranchTypeOverrides
 	ContributionRegex        Option[configdomain.ContributionRegex]
 	Detached                 configdomain.Detached
@@ -100,6 +101,7 @@ func (self *NormalConfig) OverwriteWith(other configdomain.PartialConfig) Normal
 		AutoSync:                 other.AutoSync.GetOr(self.AutoSync),
 		BitbucketAppPassword:     other.BitbucketAppPassword.Or(self.BitbucketAppPassword),
 		BitbucketUsername:        other.BitbucketUsername.Or(self.BitbucketUsername),
+		BranchPrefix:             other.BranchPrefix.Or(self.BranchPrefix),
 		BranchTypeOverrides:      other.BranchTypeOverrides.Concat(self.BranchTypeOverrides),
 		ContributionRegex:        other.ContributionRegex.Or(self.ContributionRegex),
 		Detached:                 other.Detached.GetOr(self.Detached),
@@ -249,6 +251,7 @@ func DefaultNormalConfig() NormalConfig {
 		AutoSync:             true,
 		BitbucketAppPassword: None[forgedomain.BitbucketAppPassword](),
 		BitbucketUsername:    None[forgedomain.BitbucketUsername](),
+		BranchPrefix:         None[configdomain.BranchPrefix](),
 		BranchTypeOverrides:  configdomain.BranchTypeOverrides{},
 		ContributionRegex:    None[configdomain.ContributionRegex](),
 		Detached:             false,
@@ -301,6 +304,7 @@ func NewNormalConfigFromPartial(partial configdomain.PartialConfig, defaults Nor
 		AutoSync:                 partial.AutoSync.GetOr(defaults.AutoSync),
 		BitbucketAppPassword:     partial.BitbucketAppPassword,
 		BitbucketUsername:        partial.BitbucketUsername,
+		BranchPrefix:             partial.BranchPrefix,
 		BranchTypeOverrides:      partial.BranchTypeOverrides,
 		ContributionRegex:        partial.ContributionRegex,
 		Detached:                 partial.Detached.GetOr(defaults.Detached),
diff --git a/internal/config/parse_snapshot.go b/internal/config/parse_snapshot.go
index 2d3d7e05f..74ba2d306 100644
--- a/internal/config/parse_snapshot.go
+++ b/internal/config/parse_snapshot.go
@@ -83,6 +83,7 @@ func NewLineageFromSnapshot(snapshot configdomain.SingleSnapshot, updateOutdated
 func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOutdated bool, ignoreUnknown bool, runner subshelldomain.Runner) (configdomain.PartialConfig, error) {
 	autoResolve, errAutoResolve := load(snapshot, configdomain.KeyAutoResolve, gohacks.ParseBoolOpt[configdomain.AutoResolve], ignoreUnknown)
 	autoSync, errAutoSync := load(snapshot, configdomain.KeyAutoSync, gohacks.ParseBoolOpt[configdomain.AutoSync], ignoreUnknown)
+	branchPrefix, errBranchPrefix := load(snapshot, configdomain.KeyBranchPrefix, configdomain.ParseBranchPrefix, ignoreUnknown)
 	branchTypeOverrides, errBranchTypeOverride := NewBranchTypeOverridesInSnapshot(snapshot, ignoreUnknown, runner)
 	contributionRegex, errContributionRegex := load(snapshot, configdomain.KeyContributionRegex, configdomain.ParseContributionRegex, ignoreUnknown)
 	detached, errDetached := load(snapshot, configdomain.KeyDetached, gohacks.ParseBoolOpt[configdomain.Detached], ignoreUnknown)
@@ -115,6 +116,7 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 	err := cmp.Or(
 		errAutoResolve,
 		errAutoSync,
+		errBranchPrefix,
 		errBranchTypeOverride,
 		errContributionRegex,
 		errDetached,
@@ -149,6 +151,7 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     forgedomain.ParseBitbucketAppPassword(snapshot[configdomain.KeyBitbucketAppPassword]),
 		BitbucketUsername:        forgedomain.ParseBitbucketUsername(snapshot[configdomain.KeyBitbucketUsername]),
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      branchTypeOverrides,
 		ForgejoToken:             forgedomain.ParseForgejoToken(snapshot[configdomain.KeyForgejoToken]),
 		ContributionRegex:        contributionRegex,
@@ -192,10 +195,15 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 }
 
 func load[T any](snapshot configdomain.SingleSnapshot, key configdomain.Key, parseFunc func(string, string) (T, error), ignoreUnknown bool) (T, error) { //nolint:ireturn
-	value, err := parseFunc(snapshot[key], key.String())
+	valueStr, has := snapshot[key]
+	if !has {
+		var zero T
+		return zero, nil
+	}
+	value, err := parseFunc(valueStr, key.String())
 	if err != nil {
 		if ignoreUnknown {
-			fmt.Printf("Ignoring invalid value for %q: %q\n", key, snapshot[key])
+			fmt.Printf("Ignoring invalid value for %q: %q\n", key, valueStr)
 			var zero T
 			return zero, nil
 		}
diff --git a/internal/messages/en.go b/internal/messages/en.go
index 7e182d69f..27f2763c8 100644
--- a/internal/messages/en.go
+++ b/internal/messages/en.go
@@ -51,6 +51,8 @@ const (
 	BranchNotInSyncWithParent       = `branch %q is not in sync with its parent, please run "git town sync" and try again`
 	BranchOtherWorktree             = `branch %q is active in another worktree`
 	BranchParentChanged             = "branch %q is now a child of %q"
+	BranchPrefixPrompt              = "Branch prefix: "
+	BranchPrefixResult              = "Branch prefix: %s\n"
 	BranchTypeCannotDetermine       = "cannot determine type of branch %q"
 	BrowserOpen                     = "Please open in a browser: %s\n"
 
diff --git a/internal/setup/enter.go b/internal/setup/enter.go
index dae9b4709..2821b107d 100644
--- a/internal/setup/enter.go
+++ b/internal/setup/enter.go
@@ -149,6 +149,7 @@ EnterForgeData:
 	featureRegex := None[configdomain.FeatureRegex]()
 	contributionRegex := None[configdomain.ContributionRegex]()
 	observedRegex := None[configdomain.ObservedRegex]()
+	branchPrefix := None[configdomain.BranchPrefix]()
 	order := None[configdomain.Order]()
 	newBranchType := None[configdomain.NewBranchType]()
 	unknownBranchType := None[configdomain.UnknownBranchType]()
@@ -182,6 +183,10 @@ EnterForgeData:
 		if err != nil || exit {
 			return emptyResult, exit, false, err
 		}
+		branchPrefix, exit, err = enterBranchPrefix(data)
+		if err != nil || exit {
+			return emptyResult, exit, false, err
+		}
 		newBranchType, exit, err = enterNewBranchType(data)
 		if err != nil || exit {
 			return emptyResult, exit, false, err
@@ -261,6 +266,7 @@ EnterForgeData:
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     bitbucketAppPassword,
 		BitbucketUsername:        bitbucketUsername,
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{}, // the setup assistant doesn't ask for this
 		ForgejoToken:             forgejoToken,
 		ContributionRegex:        contributionRegex,
@@ -369,6 +375,17 @@ func enterBitbucketUserName(data Data) (Option[forgedomain.BitbucketUsername], d
 	})
 }
 
+func enterBranchPrefix(data Data) (Option[configdomain.BranchPrefix], dialogdomain.Exit, error) {
+	if data.Config.File.BranchPrefix.IsSome() {
+		return None[configdomain.BranchPrefix](), false, nil
+	}
+	return dialog.BranchPrefix(dialog.Args[configdomain.BranchPrefix]{
+		Global: data.Config.GitGlobal.BranchPrefix,
+		Inputs: data.Inputs,
+		Local:  data.Config.GitLocal.BranchPrefix,
+	})
+}
+
 func enterContributionRegex(data Data) (Option[configdomain.ContributionRegex], dialogdomain.Exit, error) {
 	if data.Config.File.ContributionRegex.IsSome() {
 		return None[configdomain.ContributionRegex](), false, nil
diff --git a/internal/setup/save.go b/internal/setup/save.go
index 1a28dcc64..e95c0d262 100644
--- a/internal/setup/save.go
+++ b/internal/setup/save.go
@@ -87,6 +87,9 @@ func saveAllToFile(userInput UserInput, existingConfigFile configdomain.PartialC
 	if gitConfig.AutoSync.IsSome() {
 		_ = gitconfig.RemoveAutoSync(runner)
 	}
+	if gitConfig.BranchPrefix.IsSome() {
+		_ = gitconfig.RemoveBranchPrefix(runner)
+	}
 	if gitConfig.ContributionRegex.IsSome() {
 		_ = gitconfig.RemoveContributionRegex(runner)
 	}
@@ -211,6 +214,11 @@ func saveAllToGit(userInput UserInput, existingGitConfig configdomain.PartialCon
 			saveAutoSync(userInput.Data.AutoSync, existingGitConfig.AutoSync, frontend),
 		)
 	}
+	if configFile.BranchPrefix.IsNone() {
+		fc.Check(
+			saveBranchPrefix(userInput.Data.BranchPrefix, existingGitConfig.BranchPrefix, frontend),
+		)
+	}
 	if configFile.Detached.IsNone() {
 		fc.Check(
 			saveDetached(userInput.Data.Detached, existingGitConfig.Detached, frontend),
@@ -346,6 +354,17 @@ func saveBitbucketUsername(valueToWriteToGit Option[forgedomain.BitbucketUsernam
 	return gitconfig.RemoveBitbucketUsername(frontend)
 }
 
+func saveBranchPrefix(valueToWriteToGit Option[configdomain.BranchPrefix], valueAlreadyInGit Option[configdomain.BranchPrefix], runner subshelldomain.Runner) error {
+	if valueToWriteToGit.Equal(valueAlreadyInGit) {
+		return nil
+	}
+	if value, has := valueToWriteToGit.Get(); has {
+		return gitconfig.SetBranchPrefix(runner, value, configdomain.ConfigScopeLocal)
+	}
+	_ = gitconfig.RemoveBranchPrefix(runner)
+	return nil
+}
+
 func saveContributionRegex(valueToWriteToGit Option[configdomain.ContributionRegex], valueAlreadyInGit Option[configdomain.ContributionRegex], runner subshelldomain.Runner) error {
 	if valueToWriteToGit.Equal(valueAlreadyInGit) {
 		return nil
diff --git a/website/src/SUMMARY.md b/website/src/SUMMARY.md
index d9ba5d4c6..146320d11 100644
--- a/website/src/SUMMARY.md
+++ b/website/src/SUMMARY.md
@@ -70,6 +70,7 @@
     - [Perennial regex](preferences/perennial-regex.md)
     - [Unknown branch type](preferences/unknown-branch-type.md)
   - [Create]()
+    - [Branch prefix](preferences/branch-prefix.md)
     - [New branch type](preferences/new-branch-type.md)
     - [Share new branches](preferences/share-new-branches.md)
     - [Stash](preferences/stash.md)
diff --git a/website/src/configuration-file.md b/website/src/configuration-file.md
index e941697ac..cb41ed6ea 100644
--- a/website/src/configuration-file.md
+++ b/website/src/configuration-file.md
@@ -20,6 +20,7 @@ perennial-regex = ""
 perennials = []
 
 [create]
+branch-prefix = ""
 new-branch-type = "feature"
 share-new-branches = "no"
 
diff --git a/website/src/preferences/branch-prefix.md b/website/src/preferences/branch-prefix.md
new file mode 100644
index 000000000..04b931627
--- /dev/null
+++ b/website/src/preferences/branch-prefix.md
@@ -0,0 +1,48 @@
+# Branch prefix
+
+When set, Git Town automatically adds this prefix to branches it creates.
+
+For example, with a branch prefix of `kevgo-`:
+
+- `git town hack example` creates branch `kevgo-example`
+- `git town append child` creates branch `kevgo-child`
+- `git town prepend parent` creates branch `kevgo-parent`
+- `git town rename other` renames the current branch to `kevgo-other`
+
+If the branch name you provide already includes the configured prefix, Git Town
+won't add it again. For instance, with prefix `kevgo-`, running
+`git town hack kevgo-example` creates `kevgo-example` (not
+`kevgo-kevgo-example`).
+
+## configure in config file
+
+In the [config file](../configuration-file.md), define the branch prefix within
+the `[create]` section:
+
+```toml
+[create]
+branch-prefix = "kevgo-"
+```
+
+## configure in Git metadata
+
+To manually set the branch prefix, use the following command:
+
+```wrap
+git config [--global] git-town.branch-prefix 'kevgo-'
+```
+
+The optional `--global` flag applies this setting to all Git repositories on
+your local machine. When not present, the setting applies to the current repo.
+
+## environment variable
+
+You can configure the branch prefix by setting the `GIT_TOWN_BRANCH_PREFIX`
+environment variable.
+
+If you want to use your GitHub username as the branch prefix, set this
+environment variable once with a call like this:
+
+```bash
+export GIT_TOWN_BRANCH_PREFIX=$(gh api user --jq '.login')
+```



Test patch:
diff --git a/internal/config/configdomain/branch_prefix_test.go b/internal/config/configdomain/branch_prefix_test.go
new file mode 100644
index 000000000..46aec61a1
--- /dev/null
+++ b/internal/config/configdomain/branch_prefix_test.go
@@ -0,0 +1,58 @@
+package configdomain_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+	"github.com/shoenig/test/must"
+)
+
+func TestBranchPrefix(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Apply", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("empty prefix", func(t *testing.T) {
+			t.Parallel()
+			prefix := configdomain.BranchPrefix("")
+			branch := gitdomain.NewLocalBranchName("feature")
+			have := prefix.Apply(branch)
+			want := gitdomain.NewLocalBranchName("feature")
+			must.EqOp(t, want, have)
+		})
+
+		t.Run("non-empty prefix", func(t *testing.T) {
+			t.Parallel()
+			prefix := configdomain.BranchPrefix("prefix-")
+			branch := gitdomain.NewLocalBranchName("feature")
+			have := prefix.Apply(branch)
+			want := gitdomain.NewLocalBranchName("prefix-feature")
+			must.EqOp(t, want, have)
+		})
+
+		t.Run("branch already contains the prefix", func(t *testing.T) {
+			t.Parallel()
+			prefix := configdomain.BranchPrefix("prefix")
+			branch := gitdomain.NewLocalBranchName("prefix-branch")
+			have := prefix.Apply(branch)
+			want := gitdomain.NewLocalBranchName("prefix-branch")
+			must.EqOp(t, want, have)
+		})
+	})
+
+	t.Run("ParseBranchPrefix", func(t *testing.T) {
+		t.Parallel()
+		tests := map[string]Option[configdomain.BranchPrefix]{
+			"":        None[configdomain.BranchPrefix](),
+			"prefix-": Some(configdomain.BranchPrefix("prefix-")),
+		}
+		for give, want := range tests {
+			have, err := configdomain.ParseBranchPrefix(give, "test")
+			must.NoError(t, err)
+			must.Eq(t, want, have)
+		}
+	})
+}
diff --git a/internal/config/configfile/load_test.go b/internal/config/configfile/load_test.go
index d7ab3b3fe..4e02390ee 100644
--- a/internal/config/configfile/load_test.go
+++ b/internal/config/configfile/load_test.go
@@ -36,6 +36,7 @@ perennial-regex = "release-.*"
 unknown-type = "prototype"
 
 [create]
+branch-prefix = "feature-"
 new-branch-type = "prototype"
 push-new-branches = true
 share-new-branches = "push"
@@ -81,6 +82,7 @@ prototype-branches = "compress"
 					UnknownType:       Ptr("prototype"),
 				},
 				Create: &configfile.Create{
+					BranchPrefix:     Ptr("feature-"),
 					NewBranchType:    Ptr("prototype"),
 					PushNewbranches:  Ptr(true),
 					ShareNewBranches: Ptr("push"),
diff --git a/internal/config/configfile/save_test.go b/internal/config/configfile/save_test.go
index 909465627..3a72491d4 100644
--- a/internal/config/configfile/save_test.go
+++ b/internal/config/configfile/save_test.go
@@ -51,6 +51,7 @@ func TestSave(t *testing.T) {
 			perennialRegex := asserts.NoError1(configdomain.ParsePerennialRegex("perennial-", "test"))
 			have := configfile.RenderTOML(configdomain.PartialConfig{
 				AutoResolve:       Some(configdomain.AutoResolve(false)),
+				BranchPrefix:      Some(configdomain.BranchPrefix("feature-")),
 				ContributionRegex: contributionRegex,
 				Detached:          Some(configdomain.Detached(true)),
 				DevRemote:         Some(gitdomain.RemoteOrigin),
@@ -98,6 +99,7 @@ perennial-regex = "perennial-"
 unknown-branch-type = "prototype"
 
 [create]
+branch-prefix = "feature-"
 new-branch-type = "prototype"
 share-new-branches = "propose"
 stash = true