# Durable cross-session state

> Warn the first time a sensitive file is edited in this project, then stay silent in every later session.

You want a hook that learns and remembers. The first time the agent edits a secrets file, env file, or migration, you warn; after that you stay quiet, even in tomorrow's session. Session state forgets at the end of each session, so it can't carry the "already warned" fact forward. [DurableState](../../reference/state-sessions.md#captain_hook.DurableState) can. A `scope="project"` model keeps one file per repo, and `Deque[256]` caps the remembered set so it never grows without bound.

``` python
"""Warn the first time a sensitive file is edited in this project -- once, across every session."""

from __future__ import annotations

from captain_hook import (
    Allow,
    BaseHookEvent,
    Deque,
    DurableState,
    Event,
    HookResult,
    Input,
    Tool,
    Warn,
    on,
)

SENSITIVE = ("migrations/", "secrets", ".env")


class WarnedPaths(DurableState, scope="project"):
    paths: Deque[256]


def is_sensitive(path: str) -> bool:
    return any(marker in path for marker in SENSITIVE)


@on(
    Event.PreToolUse,
    only_if=[Tool("Edit|Write")],
    tests={
        Input(tool="Edit", file=".env", content="API_KEY=x\n"): Warn(pattern="sensitive"),
        Input(tool="Edit", file="app/users.py", content="x = 1\n"): Allow(),
    },
)
def warn_once_per_project(evt: BaseHookEvent) -> HookResult | None:
    if not (fp := evt.file) or not is_sensitive(path := str(fp.path)):
        return None
    with WarnedPaths.mutate(evt) as state:
        if path in state.paths:
            return None
        state.paths.append(path)
    return evt.warn(f"Editing a sensitive file (`{path}`). Double-check before committing secrets.")
```

> **Note: Verified by the test suite**
>
> The inline tests assert the first-fire warn and a benign allow. The "stays silent in every later session" behavior needs durable state seeded from a prior session, which single-event inline tests can't set up. It's covered by `tests/test_durable.py`.

The handler mutates under a file lock. `WarnedPaths.mutate(evt)` loads the remembered paths, checks membership, and appends the new one, all atomic, so two concurrent sessions can't both warn for the same path or lose each other's writes.


# What it catches

``` python
Input(tool="Edit", file=".env", content="API_KEY=x\n")   # first edit of a sensitive file -- warns
```


# What it allows

``` python
Input(tool="Edit", file="app/users.py", content="x = 1\n")  # not sensitive -- never warns
# a second edit of .env in any later session -- already remembered, stays silent
```

*The warn / allow split mirrors the hook inline `tests`, so it stays true as the hook evolves.*


# Run it yourself

``` bash
uvx capt-hook --hooks docs/examples test
```


# See also

- [State](../../docs/guide/state.md)
- [Conditions](../../docs/reference/conditions.md)
