Command Hooks
Command hooks run steps and checks before and after a workflow command, such as frg issue draft or frg ask. Use them to validate prerequisites, prepare the workspace, or verify outputs every time a command runs. Workflow steps, in contrast, orchestrate several commands; see Workflow Steps Reference.
How hooks work
- You define hooks per operation in
.forge/config/commands.toml, under[[commands.operations.hooks.pre]]and[[commands.operations.hooks.post]]. - Each hook item is either a step (
kind = "step") or a check (kind = "check"). - Pre-hooks run in definition order before the agent starts.
- Post-hooks run in definition order after the command succeeds. A failing post-hook doesn't fail the command, because the agent has already finished.
- When a workflow runs a command, the command's hooks run as part of that command.
[[commands]]
target = "ask"
[[commands.operations]]
name = "default"
# Runs before the command
[[commands.operations.hooks.pre]]
kind = "check"
id = "workspace-clean"
type = "command_success"
command = "git diff --quiet"
on_false = "skip"
[[commands.operations.hooks.pre]]
kind = "step"
id = "output-dir"
type = "builtin"
action = "make_directory"
path = "output"
# Runs after the command succeeds
[[commands.operations.hooks.post]]
kind = "step"
id = "list-output"
type = "shell"
command = "ls output"
Warning:
frg config validatedoesn't check hook items. An unknown action, check type, or missing field passes validation and fails only when the command runs. Test new hooks on a sample command.
Steps
Steps perform actions that can change the environment. Set type to builtin, shell, or agent.
Built-in steps
Built-in actions are cross-platform and don't start a shell. Prefer them for file operations.
| Action | Required fields | Description |
|---|---|---|
make_directory | path | Create a directory, including parents. |
make_file | path | Create an empty file. |
remove_file | path | Delete a file. |
remove_directory | path | Delete a directory recursively. |
copy_file | source, destination | Copy a file. |
write_file | path, content | Write content to a file. |
append_file | path, content | Append content to a file. |
set_env | env_name, env_value | Set an environment variable for the command and later hooks. |
[[commands.operations.hooks.pre]]
kind = "step"
id = "set-report-dir"
type = "builtin"
action = "set_env"
env_name = "REPORT_DIR"
env_value = "output/reports"
An export in a shell step affects only that shell invocation. Use set_env to pass a variable to the command and later hooks.
Shell steps
Shell steps run command. Use command_linux, command_macos, or command_windows for platform-specific variants:
[[commands.operations.hooks.pre]]
kind = "step"
id = "install-jq"
type = "shell"
command_linux = "sudo apt-get install -y jq"
command_macos = "brew install jq"
command_windows = "choco install jq"
Warning: Shell steps run with your user permissions and can run any command. Don't interpolate untrusted input or put secrets in commands. Before you use shell hooks, read Safe Customization Guidelines.
Agent steps
Agent steps (type = "agent") send a prompt to the AI agent, and share_session_with reuses another step's session. They're mainly used in workflows. See Workflow Steps Reference.
Checks
Checks test a condition without changing state.
| Check type | Required fields | Passes when |
|---|---|---|
file_exists | path | The file exists. |
directory_exists | path | The directory exists. |
path_exists | path | The file or directory exists. |
file_contains | path, content | The file contains the substring. |
file_matches | path, pattern | The file matches the regular expression. |
env_exists | env_name | The environment variable is set. |
env_equals | env_name, env_value | The environment variable equals the value. |
command_exists | path (command name) | The command is found in PATH. |
command_success | command | The command exits with status 0. |
on_false controls what happens when a check fails:
| Value | Behavior |
|---|---|
fail | Stop with an error. |
skip | Log Check failed with on_false='skip', continuing execution and continue. |
[[commands.operations.hooks.pre]]
kind = "check"
id = "in-ci"
type = "env_equals"
env_name = "CI"
env_value = "true"
on_false = "skip"
Dependencies and conditions
| Field | Description |
|---|---|
depends_on | A step name or a list of names that must complete first. failed(<name>) means the named step or check must have failed. |
requires_check | A check that must pass for the step to run. |
[[commands.operations.hooks.pre]]
kind = "step"
id = "ci-install"
type = "shell"
command = "npm ci"
requires_check = "in-ci"
[[commands.operations.hooks.pre]]
kind = "step"
id = "local-install"
type = "shell"
command = "npm install"
depends_on = "failed(in-ci)"
Forge reports invalid references when the hooks run, for example depends_on references unknown step, requires_check references unknown check, circular dependency detected among steps, and failed() requires a step or check name.
Timeout and retry
| Field | Description |
|---|---|
timeout_seconds | Stop the step if it runs longer than this. |
retry_count | Number of retries after a failure. |
retry_delay_seconds | Wait time between retries. |
[[commands.operations.hooks.pre]]
kind = "step"
id = "fetch-schema"
type = "shell"
command = "curl -fsS https://api.example.com/schema.json -o schema.json"
timeout_seconds = 60
retry_count = 3
retry_delay_seconds = 5
Examples
Validate the environment before drafting an issue
[[commands]]
target = "issue"
[[commands.operations]]
name = "draft"
[[commands.operations.hooks.pre]]
kind = "check"
id = "git-installed"
type = "command_exists"
path = "git"
on_false = "fail"
[[commands.operations.hooks.pre]]
kind = "check"
id = "in-git-repo"
type = "command_success"
command = "git rev-parse --git-dir"
on_false = "fail"
[[commands.operations.hooks.pre]]
kind = "check"
id = "has-templates"
type = "directory_exists"
path = "templates"
on_false = "skip"
Verify output after a command
[[commands]]
target = "docs"
[[commands.operations]]
name = "create"
[[commands.operations.hooks.post]]
kind = "check"
id = "readme-exists"
type = "file_exists"
path = "README.md"
on_false = "fail"
[[commands.operations.hooks.post]]
kind = "check"
id = "readme-has-title"
type = "file_contains"
path = "README.md"
content = "# "
on_false = "skip"
Hooks and workflow steps
| Command hooks | Workflow steps | |
|---|---|---|
| Scope | One command | A multi-command workflow |
| Defined in | commands.toml | A workflow definition file |
| Runs | On every invocation of the command | Only in that workflow |
| Typical use | Validation, setup, cleanup | Orchestration |
When a workflow runs a command, the order is: workflow pre-steps, command (with its hooks), workflow post-steps.
Debugging
Forge records hook execution in the job's transaction log, .forge/work/job_<id>/logs/transaction.jsonl:
# All step executions
jq 'select(.event_type == "step_execution")' .forge/work/job_*/logs/transaction.jsonl
# Failed steps
jq 'select(.event_type == "step_execution" and .outcome == "failure")' .forge/work/job_*/logs/transaction.jsonl
# Check results
jq 'select(.event_type == "check_evaluation")' .forge/work/job_*/logs/transaction.jsonl
Step events include step_id, step_type (shell, builtin, or agent), phase, outcome (success, failure, or skipped), duration_ms, command, and error. Check events include check_type, result, and action. See Transaction Log Format.
Legacy syntax
Hook syntax other than hooks.pre and hooks.post is deprecated. Forge warns:
pre_items and post_items fields are deprecated. Use [[commands.operations.hooks.pre]] and [[commands.operations.hooks.post]] instead. The old syntax will be removed in a future version.
Operation-level pre_steps, pre_checks, post_steps, and post_checks are still accepted for backward compatibility. Use hooks.pre and hooks.post for new configuration.
When you extend a built-in operation, Forge runs the built-in hook items first and your items after them. If an operation defines both hooks.pre and the legacy syntax, hooks.pre takes precedence and Forge logs a warning. The same rule applies to hooks.post.