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
Where to configure hooks
Section titled “Where to configure hooks”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 = falseMatcher behavior
Section titled “Matcher behavior”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:
editapply_patchfind_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.
Hook fields
Section titled “Hook fields”Each [[hooks.on_save]] item supports:
| Field | Required | Description |
|---|---|---|
matcher | no | Regex matcher against the tool name. Use "edit" for all file edit operations. |
directories | no | Directory prefixes, relative to the config file root. The hook only runs for files under those paths. |
extensions | no | File extensions that the hook should match, without leading dots. |
command | yes | Shell command to run. |
async | no | If true, starts in background and does not block the tool call. |
Scoped hooks by subfolder
Section titled “Scoped hooks by subfolder”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 = falseHow 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.tomlfiles 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.
File filters
Section titled “File filters”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 = falseHow filtering works:
directoriesis relative to the config file’s directory.- A directory filter matches that folder and all nested paths.
extensionsis matched case-insensitively.- If both are set, both must match.
Placeholders and environment variables
Section titled “Placeholders and environment variables”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_FILECOSINE_ON_SAVE_CONFIGCOSINE_ON_SAVE_TOOL
Execution behavior
Section titled “Execution behavior”- 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_codewhen available. - Asynchronous hooks (
async = true) return immediately after the process starts and reportstarted = trueinstead of captured output. apply_patchruns hooks once for each file it writes in the patch.
Example: lint only edited file
Section titled “Example: lint only edited file”[hooks]
[[hooks.on_save]]matcher = "edit"extensions = ["ts", "tsx"]command = "npm run lint -- {file}"async = falseCommon examples
Section titled “Common examples”Go: format only edited Go files
Section titled “Go: format only edited Go files”[hooks]
[[hooks.on_save]]matcher = "edit"extensions = ["go"]command = 'gofmt -w "$COSINE_ON_SAVE_FILE"'async = falseJavaScript / 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 = falseIf 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 = falsePython: 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 = falseIf your repo does not use uv, run the tool with your normal environment manager instead.
Example: run fast checks in background
Section titled “Example: run fast checks in background”[hooks]
[[hooks.on_save]]matcher = "edit"command = "pnpm run test:quick"async = trueWhat you see in the TUI
Section titled “What you see in the TUI”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.