diff --git a/common/config.go b/common/config.go
index af9012057f7ecb667d1b983570f9883b36e91bb8..7d6c457d74845708813928f6e0d8bb9644c97f92 100644
--- a/common/config.go
+++ b/common/config.go
@@ -48,6 +48,7 @@ type RunnerConfig struct {
 
 	Shell          string `toml:"shell" json:"shell"`
 	DisableVerbose bool   `toml:"disable_verbose" json:"disable_verbose"`
+	DisablePTY     *bool  `toml:"disable_pty" json:"disable_pty"`
 
 	SSH       *ssh.Config      `toml:"ssh" json:"ssh"`
 	Docker    *DockerConfig    `toml:"docker" json:"docker"`
@@ -72,6 +73,10 @@ func (c *RunnerConfig) UniqueID() string {
 	return c.URL + c.Token
 }
 
+func (c *RunnerConfig) IsPTYEnabled() bool {
+	return c.DisablePTY == nil || !*c.DisablePTY
+}
+
 func (c *Config) LoadConfig(configFile string) error {
 	info, err := os.Stat(configFile)
 	if err != nil {
diff --git a/executors/shell/executor_shell.go b/executors/shell/executor_shell.go
index 421c5d339aa019450b155ea0077b1385ba984931..47ef17a8f0621f9fe54de0d3d43b4e0e1b887465 100644
--- a/executors/shell/executor_shell.go
+++ b/executors/shell/executor_shell.go
@@ -8,15 +8,19 @@ import (
 	"os/exec"
 	"path/filepath"
 
+	krpty "github.com/kr/pty"
+
 	"github.com/ayufan/gitlab-ci-multi-runner/common"
 	"github.com/ayufan/gitlab-ci-multi-runner/executors"
 	"github.com/ayufan/gitlab-ci-multi-runner/helpers"
+	"io"
 )
 
 type ShellExecutor struct {
 	executors.AbstractExecutor
 	cmd       *exec.Cmd
 	scriptDir string
+	pty       *os.File
 }
 
 func (s *ShellExecutor) Prepare(config *common.RunnerConfig, build *common.Build) error {
@@ -51,6 +55,21 @@ func (s *ShellExecutor) Start() error {
 	s.cmd.Stdout = s.BuildLog
 	s.cmd.Stderr = s.BuildLog
 
+	// Open PTY if available
+	if s.Config.IsPTYEnabled() {
+		pty, tty, err := krpty.Open()
+		if err == nil {
+			go io.Copy(s.BuildLog, pty)
+			defer tty.Close()
+			s.cmd.Stdout = tty
+			s.cmd.Stderr = tty
+			s.pty = pty
+		} else if err != krpty.ErrUnsupported {
+			s.Errorln("Failed to open pty", err)
+		}
+	}
+
+	// Save shell script as file
 	if s.ShellScript.PassFile {
 		scriptDir, err := ioutil.TempDir("", "build_script")
 		if err != nil {
@@ -89,6 +108,10 @@ func (s *ShellExecutor) Cleanup() {
 		os.RemoveAll(s.scriptDir)
 	}
 
+	if s.pty != nil {
+		s.pty.Close()
+	}
+
 	s.AbstractExecutor.Cleanup()
 }