diff --git a/docs/detectors.md b/docs/detectors.md
index d737373..e3e744a 100644
--- a/docs/detectors.md
+++ b/docs/detectors.md
@@ -248,11 +248,27 @@ disables saving (backward compatible).
The detector name is automatically appended to `path`, so `path: ./state` for a detector
named `NewValueDetector` writes to `./state/NewValueDetector/`.
+#### Running under systemd
+
+The default `path` is CWD-relative. systemd services usually run with CWD `/`,
+so `./state` would resolve to `/state` (wrong location, needs root). To avoid
+this, set `StateDirectory=` in your unit file — systemd creates `/var/lib/
`
+with the right ownership and exports `$STATE_DIRECTORY`, which the default `path`
+reads automatically. No explicit `path:` needed:
+
+```ini
+[Service]
+User=detectmate
+StateDirectory=detectmate # → state at /var/lib/detectmate//
+```
+
+Setting `path:` explicitly (e.g. an `s3://` URL) always overrides `$STATE_DIRECTORY`.
+
#### Fields
| Field | Type | Default | Description |
|---|---|---|---|
-| `path` | `str` | `"./state"` | Base directory or cloud URL. Detector name is appended. |
+| `path` | `str` | `$STATE_DIRECTORY` or `"./state"` | Base directory or cloud URL. Detector name is appended. Defaults to systemd's `$STATE_DIRECTORY` if set, else `./state` (see note above). |
| `interval_seconds` | `int` | `300` | Background save interval in seconds. |
| `events_until_save` | `int \| null` | `null` | Save after this many ingested events. `null` disables event-count triggering. |
| `auto_load` | `bool` | `false` | Load saved state on construction. Raises `PersistencyLoadError` if no state exists. |
diff --git a/src/detectmatelibrary/common/detector.py b/src/detectmatelibrary/common/detector.py
index ba2067b..69cc0a4 100644
--- a/src/detectmatelibrary/common/detector.py
+++ b/src/detectmatelibrary/common/detector.py
@@ -6,7 +6,9 @@
from detectmatelibrary.utils import persistency
from detectmatelibrary.common.persist import init_persistency
-from pydantic import BaseModel, ConfigDict
+import os
+
+from pydantic import BaseModel, ConfigDict, Field
from detectmatelibrary.schemas import ParserSchema, DetectorSchema
@@ -23,7 +25,15 @@
class PersistConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
- path: str = "./state"
+ # Default honors systemd's $STATE_DIRECTORY (set by StateDirectory= in the
+ # unit file) so services persist to /var/lib/ with no explicit path=.
+ # Falls back to CWD-relative ./state outside systemd. Explicit path= wins.
+ path: str = Field(
+ default_factory=lambda: next(
+ (p for p in os.environ.get("STATE_DIRECTORY", "").split(":") if p.strip()),
+ "./state",
+ )
+ )
interval_seconds: int = 300
events_until_save: int | None = None
auto_load: bool = False
diff --git a/tests/detectmatelibrary/test_common/test_persist_config.py b/tests/detectmatelibrary/test_common/test_persist_config.py
index 9b38b3b..49d2e1b 100644
--- a/tests/detectmatelibrary/test_common/test_persist_config.py
+++ b/tests/detectmatelibrary/test_common/test_persist_config.py
@@ -12,7 +12,8 @@
class TestPersistConfig:
- def test_defaults(self):
+ def test_defaults(self, monkeypatch):
+ monkeypatch.delenv("STATE_DIRECTORY", raising=False)
cfg = PersistConfig()
assert cfg.path == "./state"
assert cfg.interval_seconds == 300
@@ -35,6 +36,25 @@ def test_extra_fields_rejected(self):
PersistConfig(unknown_field="value") # type: ignore
+class TestPersistConfigStateDirectoryDefault:
+ def test_uses_state_directory_env_when_set(self, monkeypatch):
+ monkeypatch.setenv("STATE_DIRECTORY", "/var/lib/detectmate")
+ assert PersistConfig().path == "/var/lib/detectmate"
+
+ def test_takes_first_path_when_state_directory_is_colon_list(self, monkeypatch):
+ # systemd sets STATE_DIRECTORY colon-separated for multiple StateDirectory= entries
+ monkeypatch.setenv("STATE_DIRECTORY", "/var/lib/a:/var/lib/b")
+ assert PersistConfig().path == "/var/lib/a"
+
+ def test_falls_back_to_state_when_env_unset(self, monkeypatch):
+ monkeypatch.delenv("STATE_DIRECTORY", raising=False)
+ assert PersistConfig().path == "./state"
+
+ def test_explicit_path_overrides_state_directory_env(self, monkeypatch):
+ monkeypatch.setenv("STATE_DIRECTORY", "/var/lib/detectmate")
+ assert PersistConfig(path="s3://bucket/state").path == "s3://bucket/state"
+
+
class TestCoreDetectorConfigPersistField:
def test_persist_is_none_by_default(self):
cfg = CoreDetectorConfig()