diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 0000000000..68678e7e03
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -0,0 +1,10 @@
+# yaml-language-server: $schema=https://json.schemastore.org/pre-commit-hooks.json
+
+- id: task
+ name: task
+ description: Run a Taskfile task as a pre-commit hook
+ language: golang
+ entry: task
+ pass_filenames: false
+ require_serial: true
+ minimum_pre_commit_version: "3.0.0"
diff --git a/precommit_test.go b/precommit_test.go
new file mode 100644
index 0000000000..65638b81ec
--- /dev/null
+++ b/precommit_test.go
@@ -0,0 +1,48 @@
+package task_test
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.yaml.in/yaml/v3"
+)
+
+type preCommitHook struct {
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Description string `yaml:"description"`
+ Language string `yaml:"language"`
+ Entry string `yaml:"entry"`
+ PassFilenames *bool `yaml:"pass_filenames"`
+ RequireSerial *bool `yaml:"require_serial"`
+ MinimumPreCommitVersion string `yaml:"minimum_pre_commit_version"`
+ Types []string `yaml:"types"`
+ Args []string `yaml:"args"`
+}
+
+func TestPreCommitHooksFile(t *testing.T) {
+ t.Parallel()
+
+ data, err := os.ReadFile(".pre-commit-hooks.yaml")
+ require.NoError(t, err, ".pre-commit-hooks.yaml should exist")
+
+ var hooks []preCommitHook
+ err = yaml.Unmarshal(data, &hooks)
+ require.NoError(t, err, ".pre-commit-hooks.yaml should be valid YAML")
+
+ require.Len(t, hooks, 1, "should define exactly one hook")
+
+ hook := hooks[0]
+ assert.Equal(t, "task", hook.ID, "hook id should be 'task'")
+ assert.NotEmpty(t, hook.Name, "hook name should not be empty")
+ assert.NotEmpty(t, hook.Description, "hook description should not be empty")
+ assert.Equal(t, "golang", hook.Language, "hook language should be 'golang'")
+ assert.Equal(t, "task", hook.Entry, "hook entry should be 'task'")
+ require.NotNil(t, hook.PassFilenames, "pass_filenames should be set")
+ assert.False(t, *hook.PassFilenames, "pass_filenames should be false")
+ require.NotNil(t, hook.RequireSerial, "require_serial should be set")
+ assert.True(t, *hook.RequireSerial, "require_serial should be true")
+ assert.Equal(t, "3.0.0", hook.MinimumPreCommitVersion, "minimum_pre_commit_version should be '3.0.0'")
+}
diff --git a/website/src/docs/changelog.md b/website/src/docs/changelog.md
index 570e8dff32..853ff7f5e1 100644
--- a/website/src/docs/changelog.md
+++ b/website/src/docs/changelog.md
@@ -8,6 +8,11 @@ editLink: false
::: v-pre
+## next
+
+- Added official [pre-commit](https://pre-commit.com/) support via a
+ `.pre-commit-hooks.yaml` at the repository root (#2562).
+
## v3.49.1 - 2026-03-08
* Reverted #2632 for now, which caused some regressions. That change will be
diff --git a/website/src/docs/integrations.md b/website/src/docs/integrations.md
index de9f99521b..dd434ea7d2 100644
--- a/website/src/docs/integrations.md
+++ b/website/src/docs/integrations.md
@@ -2,7 +2,7 @@
title: Integrations
description:
Official and community integrations for Task, including VS Code, JSON schemas,
- and other tools
+ pre-commit, and other tools
outline: deep
---
@@ -106,6 +106,122 @@ These files are automatically generated and kept in sync with the documentation,
ensuring AI assistants always have access to the latest Task features and usage
patterns.
+## pre-commit
+
+Task has official support for [pre-commit](https://pre-commit.com/), a framework
+for managing and maintaining multi-language pre-commit hooks. This allows you to
+automatically run Task tasks as part of your Git workflow.
+
+### Setup
+
+Add the following to your `.pre-commit-config.yaml`:
+
+```yaml
+repos:
+ - repo: https://github.com/go-task/task
+ rev: v3.x.x # Replace with the desired Task version
+ hooks:
+ - id: task
+ args: ['my-task']
+```
+
+The hook will install Task via `go install` and run the specified task. You can
+use any `task` CLI arguments in the `args` field.
+
+### Configuration
+
+The `task` hook supports the following pre-commit options:
+
+| Option | Default | Description |
+| ---------------- | ------- | ----------------------------------------------------------- |
+| `args` | `[]` | Arguments passed to `task` (e.g. task name, `--dir`, flags) |
+| `files` | `''` | Only run the hook when these files change |
+
+### Examples
+
+
+Run a task unconditionally on every commit
+
+```yaml
+repos:
+ - repo: https://github.com/go-task/task
+ rev: v3.x.x
+ hooks:
+ - id: task
+ args: ['lint']
+```
+
+
+
+
+Run a task only when certain files change
+
+```yaml
+repos:
+ - repo: https://github.com/go-task/task
+ rev: v3.x.x
+ hooks:
+ - id: task
+ files: ^docs/
+ args: ['generate-docs']
+```
+
+
+
+
+Run a task in a subdirectory
+
+```yaml
+repos:
+ - repo: https://github.com/go-task/task
+ rev: v3.x.x
+ hooks:
+ - id: task
+ args: ['--dir', 'frontend', 'build']
+```
+
+
+
+
+Run multiple tasks with different file triggers
+
+```yaml
+repos:
+ - repo: https://github.com/go-task/task
+ rev: v3.x.x
+ hooks:
+ - id: task
+ name: lint
+ files: \.go$
+ args: ['lint']
+ - id: task
+ name: generate
+ files: \.proto$
+ args: ['generate']
+```
+
+
+
+
+Run a lightweight task on commit and a heavier task on push
+
+```yaml
+repos:
+ - repo: https://github.com/go-task/task
+ rev: v3.x.x
+ hooks:
+ - id: task
+ name: lint
+ args: ['lint']
+ stages: [pre-commit]
+ - id: task
+ name: test
+ args: ['test']
+ stages: [pre-push]
+```
+
+
+
## Community Integrations
In addition to our official integrations, there is an amazing community of