diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
index 138c4a737c..0cd06e68d9 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt
@@ -128,6 +128,7 @@ class AutofillSaveActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val repo = PasswordRepository.getRepositoryDirectory()
+ val saveRoot = AutofillPreferences.saveDirectory(this)?.let(repo::resolve) ?: repo
val saveIntent =
Intent(this, PasswordCreationActivity::class.java).apply {
putExtras(
@@ -136,7 +137,7 @@ class AutofillSaveActivity : AppCompatActivity() {
putString(BasePGPActivity.EXTRA_REPO_PATH, repo.absolutePath)
putString(
BasePGPActivity.EXTRA_FILE_PATH,
- repo
+ saveRoot
.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME) ?: throw NullPointerException())
.absolutePath,
)
diff --git a/app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt
index ce009a1071..684a09cc11 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/AutofillSettings.kt
@@ -109,6 +109,18 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
summaryProvider = { activity.getString(R.string.preference_custom_public_suffixes_summary) }
textInputHintRes = R.string.preference_custom_public_suffixes_hint
}
+ editText(PreferenceKeys.AUTOFILL_SAVE_DIRECTORY) {
+ dependency = PreferenceKeys.AUTOFILL_ENABLE
+ titleRes = R.string.preference_autofill_save_directory_title
+ summaryProvider = { value ->
+ activity.getString(
+ R.string.preference_autofill_save_directory_summary,
+ value?.takeUnless { it.isBlank() }
+ ?: activity.getString(R.string.preference_autofill_save_directory_root_placeholder),
+ )
+ }
+ textInputHintRes = R.string.preference_autofill_save_directory_hint
+ }
}
}
}
diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt
index f9298b2c11..4b367efecc 100644
--- a/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt
+++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillPreferences.kt
@@ -22,6 +22,34 @@ object AutofillPreferences {
return DirectoryStructure.fromValue(value)
}
+ /**
+ * The configured relative directory Autofill-saved credentials should be placed under. Backed by
+ * [PreferenceKeys.AUTOFILL_SAVE_DIRECTORY]. Returns null when unset, or when the configured value
+ * is not a valid relative path (absolute, or containing "." or ".." components) — callers should
+ * treat a null result as the repository root itself and resolve a non-null result against it.
+ * This only affects saves made through the Autofill framework (the system "Save to Password
+ * Store?" prompt) — it has no effect on entries created from within the app.
+ */
+ fun saveDirectory(context: Context): String? {
+ return sanitizeSaveDirectory(
+ context.sharedPrefs.getString(PreferenceKeys.AUTOFILL_SAVE_DIRECTORY)
+ )
+ }
+
+ /**
+ * Validates [value] as a relative path with no "." or ".." components, returning it unchanged if
+ * valid. Invalid or blank input is rejected outright (returns null) rather than being mutated
+ * into some other path, so a rejected value always falls back to the repository root instead of
+ * silently landing somewhere the user didn't ask for.
+ */
+ internal fun sanitizeSaveDirectory(value: String?): String? {
+ if (value.isNullOrBlank()) return null
+ if (value.startsWith('/')) return null
+ val segments = value.split('/')
+ if (segments.any { it.isEmpty() || it == "." || it == ".." }) return null
+ return segments.joinToString("/")
+ }
+
fun strictDomainSearch(context: Context): Boolean {
return context.sharedPrefs.getBoolean(PreferenceKeys.STRICT_DOMAIN_SEARCH, true)
}
diff --git a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt
index c090b7cc0f..5261d0c57f 100644
--- a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt
+++ b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt
@@ -58,6 +58,7 @@ object PreferenceKeys {
const val OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES = "oreo_autofill_custom_public_suffixes"
const val OREO_AUTOFILL_DEFAULT_USERNAME = "oreo_autofill_default_username"
const val DIRECTORY_STRUCTURE = "oreo_autofill_directory_structure"
+ const val AUTOFILL_SAVE_DIRECTORY = "oreo_autofill_save_directory"
const val STRICT_DOMAIN_SEARCH = "oreo_autofill_strict_domain_search"
const val PREF_KEY_PWGEN_TYPE = "pref_key_pwgen_type"
const val REPOSITORY_INITIALIZED = "repository_initialized"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cb18607fe7..88d50e883b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -330,6 +330,10 @@
Custom domains
Autofill will distinguish subdomains of these domains.
company.com\npersonal.com
+ Save directory
+ Credentials saved via Autofill are placed under this folder instead of the store root. Currently: %1$s
+ e.g. www
+ store root
Password item edited
diff --git a/app/src/test/java/app/passwordstore/util/autofill/AutofillPreferencesTest.kt b/app/src/test/java/app/passwordstore/util/autofill/AutofillPreferencesTest.kt
new file mode 100644
index 0000000000..ce1e5bd4db
--- /dev/null
+++ b/app/src/test/java/app/passwordstore/util/autofill/AutofillPreferencesTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package app.passwordstore.util.autofill
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class AutofillPreferencesTest {
+
+ @Test
+ fun nullOrEmptyFallsBackToRoot() {
+ assertNull(AutofillPreferences.sanitizeSaveDirectory(null))
+ assertNull(AutofillPreferences.sanitizeSaveDirectory(""))
+ assertNull(AutofillPreferences.sanitizeSaveDirectory(" "))
+ }
+
+ @Test
+ fun acceptsSingleSegment() {
+ assertEquals("www", AutofillPreferences.sanitizeSaveDirectory("www"))
+ }
+
+ @Test
+ fun acceptsNestedSegments() {
+ assertEquals("www/personal", AutofillPreferences.sanitizeSaveDirectory("www/personal"))
+ }
+
+ @Test
+ fun rejectsAbsolutePath() {
+ assertNull(AutofillPreferences.sanitizeSaveDirectory("/www"))
+ }
+
+ @Test
+ fun rejectsLeadingTraversal() {
+ assertNull(AutofillPreferences.sanitizeSaveDirectory("../www"))
+ }
+
+ @Test
+ fun rejectsEmbeddedTraversal() {
+ assertNull(AutofillPreferences.sanitizeSaveDirectory("www/../personal"))
+ }
+
+ @Test
+ fun rejectsCurrentDirectoryComponent() {
+ assertNull(AutofillPreferences.sanitizeSaveDirectory("."))
+ assertNull(AutofillPreferences.sanitizeSaveDirectory("www/./personal"))
+ }
+
+ @Test
+ fun rejectsEmptyPathComponents() {
+ assertNull(AutofillPreferences.sanitizeSaveDirectory("www//personal"))
+ assertNull(AutofillPreferences.sanitizeSaveDirectory("www/"))
+ }
+}