Skip to content

Hooks

Use hooks to run commands automatically after Cosine edits files.

This is useful for workflows like:

  • running linters after edits
  • formatting changed files
  • kicking off quick test commands

Hooks are configured in your repository config:

  • cosine.toml
  • or .cosine.toml

For new repositories, cos init can also scaffold starter on_save hooks when it recognizes the repo’s tooling.

Example:

[hooks]
[[hooks.on_save]]
matcher = "edit"
directories = ["apps/website"]
extensions = ["ts", "tsx"]
command = "pnpm exec eslint --fix --no-warn-ignored \"$COSINE_ON_SAVE_FILE\""
async = false

matcher is a regex matched against the tool name that wrote the file.

For most workflows, you only need:

  • matcher = "edit"

This single matcher applies to all file edit operations:

  • edit
  • apply_patch
  • find_replace_in_file

You do not need to list tool names yourself.

matcher = "apply_patch" only matches apply_patch.

If matcher is omitted or empty, the hook runs for every on-save-capable file-writing tool. That behavior mainly exists so older on_save = "..." configs continue to work after automatic migration; for new configs, prefer matcher = "edit" for clarity.

Each [[hooks.on_save]] item supports:

FieldRequiredDescription
matchernoRegex matcher against the tool name. Use "edit" for all file edit operations.
directoriesnoDirectory prefixes, relative to the config file root. The hook only runs for files under those paths.
extensionsnoFile extensions that the hook should match, without leading dots.
commandyesShell command to run.
asyncnoIf true, starts in background and does not block the tool call.

You can define different hook behavior for different repository areas from your top-level cosine.toml.

Use a scoped table path, then add hooks.on_save entries inside it:

["/services/api".hooks]
[["/services/api".hooks.on_save]]
matcher = "edit"
command = "pnpm run lint"
async = false
["/services/ccode".hooks]
[["/services/ccode".hooks.on_save]]
matcher = "edit"
command = "gofmt -w {file}"
async = false

How it works:

  • Scope paths are evaluated relative to the repository root config.
  • A scope applies to that folder and all nested subfolders.
  • When multiple scopes match, the most specific (longest path) wins.
  • Local cosine.toml files still override parent settings in their folder tree.

Scope blocks are not limited to hooks. You can also scope other config sections such as agent fields (model, extra_context) using the same path pattern.

Use native filters instead of shell case guards when a hook only applies to certain files.

[hooks]
[[hooks.on_save]]
matcher = "edit"
directories = ["apps/website"]
extensions = ["ts", "tsx"]
command = "pnpm exec eslint --fix --no-warn-ignored \"$COSINE_ON_SAVE_FILE\""
async = false

How filtering works:

  • directories is relative to the config file’s directory.
  • A directory filter matches that folder and all nested paths.
  • extensions is matched case-insensitively.
  • If both are set, both must match.

command supports file placeholders:

  • {file}
  • {{file}}

Both are replaced with the absolute path of the edited file.

Hooks also get these environment variables:

  • COSINE_ON_SAVE_FILE
  • COSINE_ON_SAVE_CONFIG
  • COSINE_ON_SAVE_TOOL
  • Hooks run through sh -c.
  • The working directory is the directory containing the resolved config file.
  • Synchronous hooks (async = false, or omitted) block the file-writing tool until the command finishes.
  • Synchronous hook results include captured stdout/stderr, and failures include the shell error plus an exit_code when available.
  • Asynchronous hooks (async = true) return immediately after the process starts and report started = true instead of captured output.
  • apply_patch runs hooks once for each file it writes in the patch.
[hooks]
[[hooks.on_save]]
matcher = "edit"
extensions = ["ts", "tsx"]
command = "npm run lint -- {file}"
async = false
[hooks]
[[hooks.on_save]]
matcher = "edit"
extensions = ["go"]
command = 'gofmt -w "$COSINE_ON_SAVE_FILE"'
async = false

JavaScript / TypeScript: run Prettier on supported files

Section titled “JavaScript / TypeScript: run Prettier on supported files”
[hooks]
[[hooks.on_save]]
matcher = "edit"
extensions = ["js", "jsx", "ts", "tsx", "json", "md", "css", "scss", "yaml", "yml"]
command = 'pnpm exec prettier --write "$COSINE_ON_SAVE_FILE"'
async = false

If your repo uses npm instead of pnpm, replace pnpm exec with npx.

JavaScript / TypeScript: run Biome on supported files

Section titled “JavaScript / TypeScript: run Biome on supported files”
[hooks]
[[hooks.on_save]]
matcher = "edit"
extensions = ["js", "jsx", "ts", "tsx", "json"]
command = 'pnpm exec biome format --write "$COSINE_ON_SAVE_FILE"'
async = false

Python: format edited Python files with Ruff

Section titled “Python: format edited Python files with Ruff”
[hooks]
[[hooks.on_save]]
matcher = "edit"
extensions = ["py"]
command = 'uv run ruff format "$COSINE_ON_SAVE_FILE"'
async = false

If your repo does not use uv, run the tool with your normal environment manager instead.

[hooks]
[[hooks.on_save]]
matcher = "edit"
command = "pnpm run test:quick"
async = true

After a tool call completes, timeline entries show hook status:

  • hook summary line (on_save hooks: ...)
  • expanded details for each hook (hook ran, hook started, hook failed)

Use ctrl+e on a tool entry to expand details.