diff --git a/pkg/runner/action_cache.go b/pkg/runner/action_cache.go
new file mode 100644
index 0000000..1173206
--- /dev/null
+++ b/pkg/runner/action_cache.go
@@ -0,0 +1,177 @@
+package runner
+
+import (
+	"archive/tar"
+	"context"
+	"crypto/rand"
+	"encoding/hex"
+	"errors"
+	"io"
+	"io/fs"
+	"path"
+	"strings"
+
+	git "github.com/go-git/go-git/v5"
+	config "github.com/go-git/go-git/v5/config"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/go-git/go-git/v5/plumbing/transport"
+	"github.com/go-git/go-git/v5/plumbing/transport/http"
+)
+
+type ActionCache interface {
+	Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error)
+	GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error)
+}
+
+type GoGitActionCache struct {
+	Path string
+}
+
+func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
+	gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
+	gogitrepo, err := git.PlainInit(gitPath, true)
+	if errors.Is(err, git.ErrRepositoryAlreadyExists) {
+		gogitrepo, err = git.PlainOpen(gitPath)
+	}
+	if err != nil {
+		return "", err
+	}
+	tmpBranch := make([]byte, 12)
+	if _, err := rand.Read(tmpBranch); err != nil {
+		return "", err
+	}
+	branchName := hex.EncodeToString(tmpBranch)
+	var refSpec config.RefSpec
+	spec := config.RefSpec(ref + ":" + branchName)
+	tagOrSha := false
+	if spec.IsExactSHA1() {
+		refSpec = spec
+	} else if strings.HasPrefix(ref, "refs/") {
+		refSpec = config.RefSpec(ref + ":refs/heads/" + branchName)
+	} else {
+		tagOrSha = true
+		refSpec = config.RefSpec("refs/*/" + ref + ":refs/heads/*/" + branchName)
+	}
+	var auth transport.AuthMethod
+	if token != "" {
+		auth = &http.BasicAuth{
+			Username: "token",
+			Password: token,
+		}
+	}
+	remote, err := gogitrepo.CreateRemoteAnonymous(&config.RemoteConfig{
+		Name: "anonymous",
+		URLs: []string{
+			url,
+		},
+	})
+	if err != nil {
+		return "", err
+	}
+	defer func() {
+		if refs, err := gogitrepo.References(); err == nil {
+			_ = refs.ForEach(func(r *plumbing.Reference) error {
+				if strings.Contains(r.Name().String(), branchName) {
+					return gogitrepo.DeleteBranch(r.Name().String())
+				}
+				return nil
+			})
+		}
+	}()
+	if err := remote.FetchContext(ctx, &git.FetchOptions{
+		RefSpecs: []config.RefSpec{
+			refSpec,
+		},
+		Auth:  auth,
+		Force: true,
+	}); err != nil {
+		return "", err
+	}
+	if tagOrSha {
+		for _, prefix := range []string{"refs/heads/tags/", "refs/heads/heads/"} {
+			hash, err := gogitrepo.ResolveRevision(plumbing.Revision(prefix + branchName))
+			if err == nil {
+				return hash.String(), nil
+			}
+		}
+	}
+	hash, err := gogitrepo.ResolveRevision(plumbing.Revision(branchName))
+	if err != nil {
+		return "", err
+	}
+	return hash.String(), nil
+}
+
+func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
+	gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
+	gogitrepo, err := git.PlainOpen(gitPath)
+	if err != nil {
+		return nil, err
+	}
+	commit, err := gogitrepo.CommitObject(plumbing.NewHash(sha))
+	if err != nil {
+		return nil, err
+	}
+	files, err := commit.Files()
+	if err != nil {
+		return nil, err
+	}
+	rpipe, wpipe := io.Pipe()
+	// Interrupt io.Copy using ctx
+	ch := make(chan int, 1)
+	go func() {
+		select {
+		case <-ctx.Done():
+			wpipe.CloseWithError(ctx.Err())
+		case <-ch:
+		}
+	}()
+	go func() {
+		defer wpipe.Close()
+		defer close(ch)
+		tw := tar.NewWriter(wpipe)
+		cleanIncludePrefix := path.Clean(includePrefix)
+		wpipe.CloseWithError(files.ForEach(func(f *object.File) error {
+			if err := ctx.Err(); err != nil {
+				return err
+			}
+			name := f.Name
+			if strings.HasPrefix(name, cleanIncludePrefix+"/") {
+				name = name[len(cleanIncludePrefix)+1:]
+			} else if cleanIncludePrefix != "." && name != cleanIncludePrefix {
+				return nil
+			}
+			fmode, err := f.Mode.ToOSFileMode()
+			if err != nil {
+				return err
+			}
+			if fmode&fs.ModeSymlink == fs.ModeSymlink {
+				content, err := f.Contents()
+				if err != nil {
+					return err
+				}
+				return tw.WriteHeader(&tar.Header{
+					Name:     name,
+					Mode:     int64(fmode),
+					Linkname: content,
+				})
+			}
+			err = tw.WriteHeader(&tar.Header{
+				Name: name,
+				Mode: int64(fmode),
+				Size: f.Size,
+			})
+			if err != nil {
+				return err
+			}
+			reader, err := f.Reader()
+			if err != nil {
+				return err
+			}
+			_, err = io.Copy(tw, reader)
+			return err
+		}))
+	}()
+	return rpipe, err
+}
diff --git a/pkg/runner/action_cache_test.go b/pkg/runner/action_cache_test.go
new file mode 100644
index 0000000..e222cfb
--- /dev/null
+++ b/pkg/runner/action_cache_test.go
@@ -0,0 +1,37 @@
+package runner
+
+import (
+	"archive/tar"
+	"bytes"
+	"context"
+	"io"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+//nolint:gosec
+func TestActionCache(t *testing.T) {
+	a := assert.New(t)
+	cache := &GoGitActionCache{
+		Path: os.TempDir(),
+	}
+	ctx := context.Background()
+	sha, err := cache.Fetch(ctx, "christopherhx/script", "https://github.com/christopherhx/script", "main", "")
+	a.NoError(err)
+	a.NotEmpty(sha)
+	atar, err := cache.GetTarArchive(ctx, "christopherhx/script", sha, "node_modules")
+	a.NoError(err)
+	a.NotEmpty(atar)
+	mytar := tar.NewReader(atar)
+	th, err := mytar.Next()
+	a.NoError(err)
+	a.NotEqual(0, th.Size)
+	buf := &bytes.Buffer{}
+	// G110: Potential DoS vulnerability via decompression bomb (gosec)
+	_, err = io.Copy(buf, mytar)
+	a.NoError(err)
+	str := buf.String()
+	a.NotEmpty(str)
+}