gastownhall/beads · error
httpapi: a configured role fires this workspace's hooks; thi
Error message
httpapi: a configured role fires this workspace's hooks; this server does not run hooks, so take the roles from the store beneath the hook decorator ((*storage.HookFiringStore).Unwrap)
What it means
This startup-time validation error means one or more of the issue roles (Reader, Claimer, etc.) configured on httpapi.Server come from a store that fires the workspace's on_update hooks. The HTTP server's contract is that it never runs hooks — otherwise every served mutation would spawn the user's hook subprocess. Rather than silently breaking that contract, the server refuses to start and points at (*storage.HookFiringStore).Unwrap, which hands back the store beneath the hook decorator.
Source
Thrown at internal/httpapi/server.go:665
}
func checkDatabaseSource(cfg Config) error {
switch {
case cfg.Provider != nil && (anyRoleSet(cfg) || cfg.EventsJournal != nil):
return errors.New("httpapi: both a unit-of-work provider and issue roles were set; pass exactly one database source")
case cfg.Provider == nil && !everyRoleSet(cfg):
return errors.New("httpapi: no database source: set Provider, or " + roleSourceNames + " together")
// The conditional role, checked where every other configuration mistake is.
// A workspace that HAS a journal and a server that cannot read it is the
// one combination that would bind, answer every other route, and fail this
// one — with a nil dereference, which is the shape checkDatabaseSource
// exists to prevent. A workspace with the journal off needs no reader and
// this says nothing about it.
case cfg.Provider == nil && cfg.EventsJournalEnabled && cfg.EventsJournal == nil:
return errors.New("httpapi: this workspace's events journal is enabled but no EventsJournal reader was configured; " +
"take one off the store (storage.EventsJournalCursor), or serve a workspace with the journal off")
case anyRoleFiresHooks(cfg):
return errors.New("httpapi: a configured role fires this workspace's hooks; " +
"this server does not run hooks, so take the roles from the store beneath the hook decorator " +
"((*storage.HookFiringStore).Unwrap)")
case uow.ProviderFiresHooks(cfg.Provider):
// The same refusal for the other database source. A provider's roles
// carry whatever the provider carries, so a hook-firing one would run a
// user's subprocess per served mutation just as a hook-firing role does.
return errors.New("httpapi: the configured provider fires this workspace's hooks; " +
"this server does not run hooks, so pass the provider beneath the hook layer " +
"(uow.UnwrapProvider)")
}
return nil
}
// Addr is the bound address, which is the only way to discover the port under
// the ephemeral default.
func (s *Server) Addr() string { return s.listener.Addr().String() }
// Serve accepts requests until ctx is canceled, then drains. It returns nilView on GitHub (pinned to 71377f2769)
Solutions
- Wrap the store's accessors with (*storage.HookFiringStore).Unwrap to get the store beneath the hook decorator before assigning roles to the server Config
- Verify with storage.RoleFiresHooks that none of the roles you pass fire hooks
- If hooks should actually fire on these mutations, do not use this server's contract — run hooks in your own layer instead
Example fix
// before
srv, err := httpapi.New(httpapi.Config{Reader: store.IssueReader(), Claimer: store.IssueClaimer()})
// after
unwrapped := store.(*storage.HookFiringStore).Unwrap()
srv, err := httpapi.New(httpapi.Config{Reader: unwrapped.IssueReader(), Claimer: unwrapped.IssueClaimer()}) Defensive patterns
Strategy: validation
Validate before calling
for _, r := range []any{cfg.Reader, cfg.Claimer, cfg.ReadyClaimer, cfg.Releaser} {
if r != nil && storage.RoleFiresHooks(r) {
return errors.New("role fires hooks; unwrap the HookFiringStore first")
}
} Type guard
func roleSafe(r any) bool { return r == nil || !storage.RoleFiresHooks(r) } Try / catch
if err := checkDatabaseSource(cfg); err != nil {
if strings.Contains(err.Error(), "fires this workspace's hooks") {
cfg = unwrapHookRoles(cfg) // (*storage.HookFiringStore).Unwrap on the source store
}
return err
} Prevention
- Always derive server roles from the unwrapped store, never from the hook-decorated one
- Add a startup assertion that storage.RoleFiresHooks is false for every configured role
- Centralize server store wiring in one constructor so unwrapping cannot be forgotten
When it happens
Trigger: Calling httpapi.New (or equivalent) with Config roles obtained directly from a storage.HookFiringStore, e.g. store.IssueClaimer() on a store wrapped in the hook decorator. checkDatabaseSource detects any role where storage.RoleFiresHooks is true at Listen/startup and returns this error.
Common situations: A developer wires bd serve with the most natural code — roles taken straight off the storage chain — without realizing the accessor on a hook-wrapped store returns hook-firing decorators. Happens after adopting hooks in a workspace, or when copying server setup from code that intentionally fires hooks.
Related errors
- httpapi: the configured provider fires this workspace's hook
- httpapi: this server has no unit-of-work provider; it answer
- --allowed-host is empty; pass the Host header value clients
- multiple .doltcfg directories detected
- dolt directory is required
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/728e0add3da2edbc.
Report an issue: GitHub.