Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/mdbook/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
- [`fail_text`](./configuration/fail_text.md)
- [`stage_fixed`](./configuration/stage_fixed.md)
- [`interactive`](./configuration/interactive.md)
- [`stream`](./configuration/stream.md)
- [`use_stdin`](./configuration/use_stdin.md)
- [`commands`](./configuration/Commands.md)
- [`run`](./configuration/run.md)
Expand All @@ -101,6 +102,7 @@
- [`fail_text`](./configuration/fail_text.md)
- [`stage_fixed`](./configuration/stage_fixed.md)
- [`interactive`](./configuration/interactive.md)
- [`stream`](./configuration/stream.md)
- [`use_stdin`](./configuration/use_stdin.md)
- [`priority`](./configuration/priority.md)
- [`scripts`](./configuration/Scripts.md)
Expand All @@ -112,6 +114,7 @@
- [`fail_text`](./configuration/fail_text.md)
- [`stage_fixed`](./configuration/stage_fixed.md)
- [`interactive`](./configuration/interactive.md)
- [`stream`](./configuration/stream.md)
- [`use_stdin`](./configuration/use_stdin.md)
- [`priority`](./configuration/priority.md)
- [ENV variables](./usage/env.md)
Expand Down
37 changes: 37 additions & 0 deletions docs/mdbook/configuration/stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## `stream`

**Default: `false`**

Enable real-time streaming output for commands or scripts without requiring a TTY. This is useful when you want to see command output as it happens in non-interactive environments like VS Code's git commit interface, CI/CD pipelines, or any environment where a TTY is not available.

Unlike [`interactive`](./interactive.md), `stream` does not attempt to open `/dev/tty` for user input, making it work seamlessly in environments without TTY access.

**Example**

Use this option when you want to see real-time output from long-running commands or scripts, such as linters or test runners, even when running commits from editors or IDEs:

```yml
# lefthook.yml
pre-commit:
commands:
tests:
run: npm test
stream: true
lint:
run: npm run lint
stream: true
```

**Comparison with other options**

- `stream: true` - Streams output in real-time, no TTY required, no user input
- `interactive: true` - Requires TTY for user input, will fail if TTY is unavailable
- `use_stdin: true` - Passes stdin to the command but doesn't control output streaming
- Default (all false) - Uses pseudo-TTY for output buffering

**When to use**

- You want real-time output visibility
- You're running hooks from non-TTY environments (VS Code, GitHub Desktop, etc.)
- Your command produces output you want to see immediately
- You don't need user input during execution
2 changes: 2 additions & 0 deletions internal/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Command struct {

Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"`
Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"`
Stream bool `json:"stream,omitempty" mapstructure:"stream" toml:"stream,omitempty" yaml:",omitempty"`
UseStdin bool `json:"use_stdin,omitempty" koanf:"use_stdin" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:"use_stdin,omitempty"`
StageFixed bool `json:"stage_fixed,omitempty" koanf:"stage_fixed" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"`
}
Expand All @@ -45,6 +46,7 @@ func CommandsToJobs(commands map[string]*Command) []*Job {
FileTypes: command.FileTypes,
Env: command.Env,
Interactive: command.Interactive,
Stream: command.Stream,
UseStdin: command.UseStdin,
StageFixed: command.StageFixed,
Exclude: command.Exclude,
Expand Down
1 change: 1 addition & 0 deletions internal/config/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Job struct {
Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"`

Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"`
Stream bool `json:"stream,omitempty" mapstructure:"stream" toml:"stream,omitempty" yaml:",omitempty"`
UseStdin bool `json:"use_stdin,omitempty" koanf:"use_stdin" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:"use_stdin,omitempty"`
StageFixed bool `json:"stage_fixed,omitempty" koanf:"stage_fixed" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"`

Expand Down
11 changes: 10 additions & 1 deletion internal/config/jsonschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
"interactive": {
"type": "boolean"
},
"stream": {
"type": "boolean"
},
"use_stdin": {
"type": "boolean"
},
Expand Down Expand Up @@ -300,6 +303,9 @@
"interactive": {
"type": "boolean"
},
"stream": {
"type": "boolean"
},
"use_stdin": {
"type": "boolean"
},
Expand Down Expand Up @@ -421,6 +427,9 @@
"interactive": {
"type": "boolean"
},
"stream": {
"type": "boolean"
},
"use_stdin": {
"type": "boolean"
},
Expand All @@ -432,7 +441,7 @@
"type": "object"
}
},
"$comment": "Last updated on 2025.10.24.",
"$comment": "Last updated on 2025.10.31.",
"properties": {
"min_version": {
"type": "string",
Expand Down
2 changes: 2 additions & 0 deletions internal/config/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Script struct {

FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"`
Stream bool `json:"stream,omitempty" mapstructure:"stream" toml:"stream,omitempty" yaml:",omitempty"`
UseStdin bool `json:"use_stdin,omitempty" koanf:"use_stdin" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:"use_stdin,omitempty"`
StageFixed bool `json:"stage_fixed,omitempty" koanf:"stage_fixed" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"`
}
Expand All @@ -34,6 +35,7 @@ func ScriptsToJobs(scripts map[string]*Script) []*Job {
Tags: script.Tags,
Env: script.Env,
Interactive: script.Interactive,
Stream: script.Stream,
UseStdin: script.UseStdin,
StageFixed: script.StageFixed,
Skip: script.Skip,
Expand Down
16 changes: 9 additions & 7 deletions internal/run/controller/exec/exec_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ import (
type CommandExecutor struct{}

type executeArgs struct {
in io.Reader
out io.Writer
envs []string
root string
interactive, useStdin bool
in io.Reader
out io.Writer
envs []string
root string
interactive, stream, useStdin bool
}

func (e CommandExecutor) Execute(ctx context.Context, opts Options, in io.Reader, out io.Writer) error {
if opts.Interactive && !isatty.IsTerminal(os.Stdin.Fd()) {
// Only try to open TTY for interactive mode (user input needed), not for stream mode
if opts.Interactive && !opts.Stream && !isatty.IsTerminal(os.Stdin.Fd()) {
tty, err := os.Open("/dev/tty")
if err == nil {
defer func() {
Expand Down Expand Up @@ -63,6 +64,7 @@ func (e CommandExecutor) Execute(ctx context.Context, opts Options, in io.Reader
envs: envs,
root: root,
interactive: opts.Interactive,
stream: opts.Stream,
useStdin: opts.UseStdin,
}

Expand All @@ -83,7 +85,7 @@ func (e CommandExecutor) execute(ctx context.Context, cmdstr string, args *execu
command.Dir = args.root
command.Env = append(os.Environ(), args.envs...)

if args.interactive || args.useStdin {
if args.interactive || args.stream || args.useStdin {
command.Stdout = args.out
command.Stdin = args.in
command.Stderr = os.Stderr
Expand Down
3 changes: 2 additions & 1 deletion internal/run/controller/exec/exec_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ type executeArgs struct {
}

func (e CommandExecutor) Execute(ctx context.Context, opts Options, in io.Reader, out io.Writer) error {
if opts.Interactive && !isatty.IsTerminal(os.Stdin.Fd()) {
// Only try to open TTY for interactive mode (user input needed), not for stream mode
if opts.Interactive && !opts.Stream && !isatty.IsTerminal(os.Stdin.Fd()) {
tty, err := tty.Open()
if err == nil {
defer tty.Close()
Expand Down
8 changes: 4 additions & 4 deletions internal/run/controller/exec/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (

// Options contains the data that controls the execution.
type Options struct {
Root string
Commands []string
Env map[string]string
Interactive, UseStdin bool
Root string
Commands []string
Env map[string]string
Interactive, Stream, UseStdin bool
}

// Executor provides an interface for command execution.
Expand Down
1 change: 1 addition & 0 deletions internal/run/controller/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func (c *Controller) runSingleJob(ctx context.Context, scope *scope, id string,
Root: filepath.Join(c.git.RootPath, scope.root),
Commands: commands,
Interactive: job.Interactive && !scope.opts.DisableTTY,
Stream: job.Stream,
UseStdin: job.UseStdin,
Env: env,
})
Expand Down
11 changes: 10 additions & 1 deletion schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
"interactive": {
"type": "boolean"
},
"stream": {
"type": "boolean"
},
"use_stdin": {
"type": "boolean"
},
Expand Down Expand Up @@ -300,6 +303,9 @@
"interactive": {
"type": "boolean"
},
"stream": {
"type": "boolean"
},
"use_stdin": {
"type": "boolean"
},
Expand Down Expand Up @@ -421,6 +427,9 @@
"interactive": {
"type": "boolean"
},
"stream": {
"type": "boolean"
},
"use_stdin": {
"type": "boolean"
},
Expand All @@ -432,7 +441,7 @@
"type": "object"
}
},
"$comment": "Last updated on 2025.10.24.",
"$comment": "Last updated on 2025.10.31.",
"properties": {
"min_version": {
"type": "string",
Expand Down