How the SessionStart Hook Initializes Sessions in Caveman: Execution Flow and Architecture

The SessionStart hook in Caveman executes once at session creation to preload configuration and data into the session context before any user prompt is processed, merging its hookSpecificOutput JSON field into the atomic session state.

In the JuliusBrussee/caveman repository, the SessionStart hook serves as the primary entry point for session initialization, allowing developers to inject pre-computed data into the conversation context without per-turn overhead. This hook triggers immediately after the proxy generates a fresh session identifier and before the first user prompt reaches the model, making it ideal for expensive operations like loading configuration files or caching remote resources according to the source code.

SessionStart Hook Execution Flow

Session Initialization and Event Detection

When Caveman’s proxy router boots a new session, it first creates a fresh session identifier used for logging, telemetry, and cache keys. In proxy/internal/gateway/server.go, the gateway validates the SessionMarkerKey to detect the session_start event and forward it to the native-hook transport layer. This validation ensures that session-specific logic only runs for genuine new session events, not for ongoing conversation turns.

Hook Invocation and Context Injection

Once the event is detected, the native hook runtime spawns the external hook process via the command caveman native-hook <Event>, passing the event JSON on stdin and reading the hook’s reply on stdout. The Run function in proxy/internal/nativehook/hook.go executes this process with a strict 250 ms deadline. The hook must return a JSON object containing a hookSpecificOutput field, which the runtime extracts and merges into the session’s initial context (SessionContext). As noted in proxy/internal/store/learn.go, this mechanism allows "SessionStart/UserPromptSubmit hooks [to] inject their output into context each session."

Architecture and Implementation Details

Proxy Router and Native Hook Runtime

The proxy layer in proxy/internal/gateway/server.go handles the initial detection of session boundaries, while proxy/internal/nativehook/hook.go contains the core Run function that manages the lifecycle of the external hook process. This implementation enforces the 250 ms timeout to prevent expensive operations from blocking session creation, and it parses the JSON output to extract the payload for context injection.

Context Persistence and Storage

After the hook returns successfully, Caveman writes the session context using atomic writes with restrictive permissions to prevent malicious project-controlled symlinks from hijacking session state. The hookSpecificOutput field is stored atomically on disk in the session store, ensuring that downstream components receive a consistent snapshot of the pre-loaded data even if concurrent modifications occur.

Downstream Consumption in the Engine

All downstream tool calls and model prompt builders receive the enriched context through the SessionContext structure. In engine/toon.go, the engine accesses ctx.HookSpecificOutput when constructing model requests, allowing prompts to reference data that was pre-loaded during the SessionStart phase. This design eliminates per-turn overhead since the expensive data loading occurs once at session creation rather than during every inference request.

Implementing a Custom SessionStart Hook

Below is a complete Go implementation that loads a local configuration file and exposes it to the session context:

// File: myhook/session_start.go
package main

import (
	"encoding/json"
	"os"
)

type HookOutput struct {
	HookSpecificOutput map[string]any `json:"hookSpecificOutput"`
}

func main() {
	// Example: load a JSON config file and expose it to the session.
	cfg, _ := os.ReadFile("config.json")
	var data map[string]any
	_ = json.Unmarshal(cfg, &data)

	out := HookOutput{
		HookSpecificOutput: map[string]any{
			"my_config": data,
		},
	}
	enc := json.NewEncoder(os.Stdout)
	_ = enc.Encode(out) // Caveman reads this from stdout.
}

Register the hook with the proxy using:

caveman native-hook myhook SessionStart

Downstream code in the engine can then access this data via the session context:

// Inside engine/toon.go (simplified)
func buildPrompt(ctx *SessionContext) string {
	// `ctx.HookSpecificOutput` contains the SessionStart hook output.
	if cfg, ok := ctx.HookSpecificOutput["my_config"]; ok {
		return fmt.Sprintf("Use this config: %v\n%s", cfg, ctx.UserPrompt)
	}
	return ctx.UserPrompt
}

Pi Extension Integration Example

The Pi extension maps its native lifecycle events to Caveman hooks in docs/plans/pi-extension.md:

session_start → SessionStart,
before_agent_start → UserPromptSubmit,
…

When Pi boots a session, it executes caveman native-hook pi SessionStart, passing the JSON payload on stdin. This allows Pi-specific initialization logic to run within Caveman’s hook framework while maintaining compatibility with the generic native-hook transport defined in the proxy runtime.

Failure Handling and Security Considerations

Fail-Open Behavior

The SessionStart hook operates on a fail-open basis: if the process times out after 250 ms or returns a non-zero exit code, Caveman logs a warning and proceeds with an empty hook payload. Because the hook runs only once per session, any failure does not affect subsequent conversation turns; the session simply continues without the pre-loaded data.

Atomic Write Protections

To prevent symlink attacks or partial writes, Caveman uses atomic file operations when persisting the session context. The restrictive permissions ensure that only the proxy process can modify the session state file, protecting against malicious attempts to inject data through filesystem manipulation.

Summary

  • The SessionStart hook in Caveman triggers once immediately after session creation, before any user prompt is processed.
  • It communicates via stdin/stdout JSON exchange with a strict 250 ms deadline enforced by proxy/internal/nativehook/hook.go.
  • The hookSpecificOutput field from the hook response is merged into SessionContext and persisted atomically in proxy/internal/store/learn.go.
  • Downstream components in engine/toon.go access this pre-loaded data when building model requests.
  • Failures are handled gracefully with fail-open behavior, while atomic writes prevent session state hijacking.

Frequently Asked Questions

What happens if the SessionStart hook exceeds the 250ms deadline?

If the hook process does not complete within 250 milliseconds, the Run function in proxy/internal/nativehook/hook.go terminates the process and logs a timeout warning. Caveman proceeds with session creation using an empty hookSpecificOutput payload, ensuring that slow initialization does not block the user experience.

Can a SessionStart hook access the filesystem or make network requests?

Yes, the SessionStart hook can perform any I/O operation available to its process, including reading files and calling APIs, provided it completes before the deadline. This capability allows the hook to load large configuration files or cache remote resources, though developers should ensure these operations respect the 250 ms timeout to avoid fail-open behavior.

How does the SessionStart hook differ from the UserPromptSubmit hook?

The SessionStart hook executes exactly once per session during initialization, making it suitable for expensive one-time setup, while the UserPromptSubmit hook runs before every user turn. According to the code comments in proxy/internal/store/learn.go, both hooks inject their hookSpecificOutput into context, but the SessionStart hook populates the initial session state whereas UserPromptSubmit handles per-turn augmentations.

Where is the hook output stored and how is it accessed by downstream components?

The hook output is stored in the atomic session context managed by proxy/internal/store/learn.go and accessed through the SessionContext structure. Components like engine/toon.go retrieve values from the HookSpecificOutput map field when constructing model prompts or executing tool calls, allowing seamless integration of pre-loaded data into the conversation flow.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →