lifecycle

Hooks

A hook is your own command, run at a fixed point in a session: before a tool call, when you submit a prompt, when a turn ends. Hooks let you audit what the agent is doing, hand it extra context, rewrite a prompt or a tool's input, and block an action outright. You write the command, and you declare it in a file you control. Anything a cloned repository could have supplied stays inert until you approve it, one hook at a time.

contents

contents

Two ways to declare a hook, gated separately

Hooks declared in hooks.json or inline [hooks.events] use hooks.config_hooks_enabled, which defaults to true. Skill-frontmatter hooks use hooks.skills_enabled, which defaults to false. These switches are separate from trust in a workspace-controlled declaring file.

Leaving the first channel on is safe because two protections do not depend on it. A committed squeezy-settings.toml cannot set any hooks key, so a repository can neither switch hook execution on for you nor contribute hook commands through configuration. And any hook whose declaring file lives inside your workspace stays inert until you approve it with squeezy hooks trust. Cloning a repository arms nothing.

  • $SQUEEZY_HOME/user/hooks.json is your personal hooks file, read in every workspace, and is a trusted source.
  • <repo>/.squeezy/hooks.json is workspace-controlled: discovered and listed, but never run until you trust it.
  • $SQUEEZY_HOME/projects/<project-dir>/config/hooks.json holds hooks for one checkout. It sits outside the repository, so it is a trusted source too.
  • An inline [hooks.events] table in your User or Local settings covers a small setup without a second file. A committed squeezy-settings.toml cannot carry one.
  • A skill's hooks: frontmatter is the fifth source, gated by [hooks] skills_enabled, which is false by default.
  • The Hooks page of /config lists everything declared and writes a new hook into any of the three files for you, so hand-editing JSON is optional.
[hooks]
config_hooks_enabled = true # default; hooks.json and [hooks.events]
skills_enabled = false      # default; hooks: in SKILL.md frontmatter

What a hook looks like

The shape is the same in both channels: an event name, an optional matcher narrowing which payloads it fires on, and one or more commands. Event keys accept either spelling of the name, PreToolUse or pre_tool_use, and the keys inside are identical in a hooks file and in SKILL.md frontmatter, so a block moves between them unchanged.

A hook receives a versioned JSON payload on standard input, and also as an owner-only file whose path arrives in SQUEEZY_HOOK_PAYLOAD_FILE. Exit status alone gives the basic allow/deny answer: zero allows, non-zero denies. A hook that exits zero can also print JSON to do more, rewriting the prompt or a tool's input, adding context or instructions for the next model step, or deciding a pending permission request. If a script already prints its answer for another agent tool, those field spellings are read too: hookSpecificOutput, permissionDecision, continue, and systemMessage are accepted alongside Squeezy's own names.

Squeezy ships the cross-platform hook runner, not the audit.sh placeholder below. Create that script yourself on POSIX; on Windows invoke a PowerShell script explicitly with pwsh and args, or install one compiled hook executable on PATH for every platform.

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "shell|write_file",
        "hooks": [
          { "type": "command",
            "id": "audit",
            "command": "./scripts/audit.sh",
            "timeout_seconds": 10,
            "spawn_failure_policy": "deny",
            "fail_open": false }
        ] }
    ]
  }
}

The 23 events

Each event fixes what your hook is allowed to do with the answer it returns, and the parentheses below say which. Enforcement events can deny the thing that is about to happen. Mutating events can rewrite an input or add context that reaches the model. Observation events cannot change the outcome at all, and exist so you can log, notify, or measure.

  • Prompt and turn: UserPromptSubmit (deny, rewrite, or add context), UserPromptExpansion (deny), PreTurn (deny or add turn instructions), Stop (request another model step), StopFailure (observe).
  • Tools: PreToolUse (deny or replace the tool input), PostToolUse and PostToolUseFailure and PostTool (add context for the next step), PostToolBatch (add aggregate context once per settled batch).
  • Permissions: PermissionRequest (allow, deny, or leave the prompt alone), PermissionDenied (observe).
  • Context: PreCompact (deny or add compaction instructions), PostCompact (observe), InstructionsLoaded (observe a newly loaded AGENTS.md or SKILL.md body).
  • Subagents: SubagentStart (observe), SubagentStop (request another subagent step).
  • Session: SessionStart and Setup (add context, once per session and once per instance), SessionEnd (observe; always awaited during shutdown), ConfigChange (observe an accepted config update).
  • MCP: Elicitation (deny presentation), ElicitationResult (observe).

Approving what a repository ships

Any hook declared inside your workspace requires explicit approval before it can run, whether it came from a checked-in hooks file or a project skill. Approval binds the exact behavior: the event, the matcher, the command and its arguments, the execution options, the environment variables the hook asked for, and a digest of the declaring file or the whole skill bundle. Change any of it and the hook goes back to needing review, and a running agent rechecks the digest immediately before it executes.

That last part matters more than it sounds. It means approving a hook is not approving a name, it is approving a specific thing the hook does. A pull request that quietly edits an approved hook's command does not inherit the approval.

  • squeezy hooks list shows every declared hook, where it came from, and whether it is trusted, modified, or blocked.
  • squeezy hooks trust <id> approves one hook; --all approves every workspace-controlled hook it can see, and untrust reverses either.
  • /hooks does the same from inside a session, and a startup notice names hooks that are pending review and any channel that is switched off.
  • A workspace skill bundle containing a symlink that escapes its own directory is marked blocked and cannot be trusted at all.
squeezy hooks list
squeezy hooks trust hook_9f3c1d70a4b28e15c6d0f3a2
squeezy hooks untrust --all

What a hook is allowed to touch

A hook command runs on your machine with the same privileges Squeezy has, and Squeezy does not sandbox it. Approving a hook is approving a program, which is why the two channel switches and the per-hook approval exist. Inside that boundary, several things are bounded so a misbehaving hook degrades instead of hanging your session.

A hook receives a small bootstrap environment plus the additional parent variables named in env_allowlist and Squeezy’s hook payload variables. Requested variable names are included in its trust review; an exported API key is not passed through merely because it exists in the launching shell.

  • Timeouts default to 30 seconds and always deny on expiry, whatever fail_open says, because a hung hook is an anomaly.
  • Each hook runs in its own process group, so a timeout reaches the commands its script spawned, not just the shell.
  • Output is captured up to a fixed size and redacted before it is shown or logged.
  • fail_open = false makes a spawn or wait failure deny, so a missing interpreter cannot quietly neutralize a policy hook.
  • An observation-only hook can set async to run off the dispatch path. On an event whose answer is enforced the flag is refused and the hook is not installed at all.

Checking your setup

squeezy doctor reports whether the hook shell is reachable, the failure people hit first on Windows, and checks each hook that is cleared to run: a script that is missing, one without an executable bit, one with no shebang line, and a command that is an inline shell snippet whose behavior depends on the system shell. It reads and inspects; it never runs a hook to find out.

If a hook is not firing, work through three causes in that order: the channel's switch is off, the hook is workspace-controlled and not yet trusted, or the matcher does not match the payload you expected. The startup notice names the first two.

squeezy doctor --only hooks:shell
the contents list reaches every page in the docs
github

Repository access is under construction.

squeezy's repository is not public yet. The product site and documentation are available here in the meantime.