aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore17
-rw-r--r--LICENSE15
-rw-r--r--Makefile42
-rw-r--r--README45
-rw-r--r--config.mk12
-rw-r--r--git-protection.165
-rw-r--r--git-protection.c98
7 files changed, 294 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8906e61
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+*\#*
+*~
+*.o
+*.a
+*.t
+*.lo
+*.to
+*.su
+*.so
+*.so.*
+*.dll
+*.dylib
+*.gch
+*.gcov
+*.gcno
+*.gcda
+/git-protection
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1634eae
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+© 2026 Mattias Andrée <m@maandree.se>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0b22ed7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,42 @@
+.POSIX:
+
+CONFIGFILE = config.mk
+include $(CONFIGFILE)
+
+OBJ =\
+ git-protection.o
+
+HDR =
+
+all: git-protection
+$(OBJ): $(HDR)
+
+.c.o:
+ $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS)
+
+git-protection: $(OBJ)
+ $(CC) -o $@ $(OBJ) $(LDFLAGS)
+ $(BUILD_SUDO) sh -c 'chown -- 0:0 $@ && chmod -- 4755 $@'
+
+install: git-protection
+ mkdir -p -- "$(DESTDIR)$(PREFIX)/bin"
+ mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/"
+ cp -- git-protection "$(DESTDIR)$(PREFIX)/bin/"
+ cp -- git-protection.1 "$(DESTDIR)$(MANPREFIX)/man1/"
+
+post-install:
+ chown -- '0:0' "$(DESTDIR)$(PREFIX)/bin/git-protection"
+ chmod -- 4755 "$(DESTDIR)$(PREFIX)/bin/git-protection"
+
+uninstall:
+ -rm -f -- "$(DESTDIR)$(PREFIX)/bin/git-protection"
+ -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/git-protection.1"
+
+clean:
+ -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda
+ -rm -f -- git-protection
+
+.SUFFIXES:
+.SUFFIXES: .o .c
+
+.PHONY: all install post-install uninstall clean
diff --git a/README b/README
new file mode 100644
index 0000000..dddde5a
--- /dev/null
+++ b/README
@@ -0,0 +1,45 @@
+NAME
+ git-protection - Spawn a new program with .git mounted as read-only
+
+SYNOPSIS
+ git-protection utility [argument] ...
+
+DESCRIPTION
+ The git-protection utility runs a specified utility, but makes
+ the .git directory a read-only mountpoint the specified utility.
+
+OPTIONS
+ No options are supported.
+
+OPERANDS
+ The following operands are supported:
+
+ utility
+ The name of the utility to be invoked.
+
+ argument
+ A string to pass as an argument for the invoked utility.
+
+EXIT STATUS
+ If utility is invoked, the exit status of git-protection is the exit
+ status of utility; otherwise, the git-protection utility exits with
+ one of the following values:
+
+ 125 An error occurred in the git-protection utility.
+
+ 126 The utility specified by utility was found but could
+ not be invoked.
+
+ 127 The utility specified by utility could not be found.
+
+RATIONALE
+ The git-protection utility can be used as a wrapper around agentic
+ aritifical intelligence tools to stop them from making destructive
+ changes to your git repository, only allowing them to write to the
+ workspace without involving git but read git data. Blocking the
+ tool from unstaging changes or otherwise remove information that
+ has been stored in git, actions that these tools otherwise like
+ to perform.
+
+SEE ALSO
+ None.
diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..b58b379
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,12 @@
+PREFIX = /usr
+MANPREFIX = $(PREFIX)/share/man
+
+GIT_PATH = /usr/bin/git
+
+CC = c99
+
+CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE -D'GIT_PATH="$(GIT_PATH)"'
+CFLAGS =
+LDFLAGS = -lsimple
+
+BUILD_SUDO = @:
diff --git a/git-protection.1 b/git-protection.1
new file mode 100644
index 0000000..db13233
--- /dev/null
+++ b/git-protection.1
@@ -0,0 +1,65 @@
+.TH GIT-PROTECTION 1 GIT-PROTECTION
+.SH NAME
+git-protection \- Spawn a new program with .git mounted as read-only
+
+.SH SYNOPSIS
+.B git-protection
+.I utility
+.RI [ argument ]\ ...
+
+.SH DESCRIPTION
+The
+.B git-protection
+utility runs a specified utility, but makes the
+.I .git
+directory a read-only mountpoint the specified utility.
+
+.SH OPTIONS
+No options are supported.
+
+.SH OPERANDS
+The following operands are supported:
+.TP
+.I utility
+The name of the utility to be invoked.
+.TP
+.I argument
+A string to pass as an argument for the invoked utility.
+
+.SH EXIT STATUS
+If
+.I utility
+is invoked, the exit status of
+.B git-protection
+is the exit status of utility; otherwise, the
+.I git-protection
+utility exits with one of the following values:
+.TP
+125
+An error occurred in the
+.I git-protection
+utility.
+.TP
+126
+The utility specified by
+.I utility
+was found but could not be invoked.
+.TP
+127
+The utility specified by
+.I utility
+could not be found.
+
+.SH RATIONALE
+The
+.I git-protection
+utility can be used as a wrapper around agentic
+aritifical intelligence tools to stop them from making destructive
+changes to your git repository, only allowing them to write to the
+workspace without involving git but read git data. Blocking the
+tool from unstaging changes or otherwise remove information that
+has been stored in git, actions that these tools otherwise like
+to perform.
+
+.SH SEE ALSO
+None.
diff --git a/git-protection.c b/git-protection.c
new file mode 100644
index 0000000..6084c74
--- /dev/null
+++ b/git-protection.c
@@ -0,0 +1,98 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/mount.h>
+#include <sched.h>
+#include <libsimple.h>
+#include <libsimple-arg.h>
+
+NUSAGE(125, "utility [argument] ...");
+
+
+int
+main(int argc, char *argv[])
+{
+ int fds[2], status;
+ pid_t pid;
+ char *dir = NULL;
+ size_t dirsize = 0;
+ size_t dirlen = 0;
+ ssize_t r;
+
+ libsimple_default_failure_exit = 125;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND;
+
+ if (!argc)
+ usage();
+
+ if (unshare(CLONE_NEWNS))
+ eprintf("unshare CLONE_NEWNS:");
+ if (mount("none", "/", NULL, MS_REC | MS_SLAVE, NULL))
+ eprintf("mount none / NULL MS_REC|MS_SLAVE NULL:");
+
+ if (pipe(fds))
+ eprintf("pipe:");
+ pid = fork();
+ if (pid < 0)
+ eprintf("fork:");
+ if (pid == 0) {
+ if (setegid(getgid()))
+ eprintf("setegid <real group>:");
+ if (seteuid(getuid()))
+ eprintf("seteuid <real user>:");
+
+ close(fds[0]);
+ if (fds[1] != STDOUT_FILENO) {
+ if (dup2(fds[1], STDOUT_FILENO) != STDOUT_FILENO)
+ eprintf("dup2 <pipe> STDOUT_FILENO:");
+ close(fds[1]);
+ }
+ execlp(GIT_PATH, "git", "rev-parse", "--show-toplevel", NULL);
+ eprintf("execlp %s:", GIT_PATH);
+ }
+ close(fds[1]);
+ for (;;) {
+ if (dirlen == dirsize)
+ dir = erealloc(dir, dirsize += 512u);
+ r = read(fds[0], &dir[dirlen], dirsize - dirlen);
+ if (r <= 0) {
+ if (!r)
+ break;
+ if (errno == EINTR)
+ continue;
+ eprintf("read <pipe>:");
+ }
+ dirlen += (size_t)r;
+ if (!dirlen || dir[--dirlen] != '\n')
+ eprintf("received invalid output from `git rev-parse --show-toplevel`");
+ }
+ close(fds[0]);
+ if (waitpid(pid, &status, 0) != pid)
+ eprintf("waitpid <subprocess>:");
+ if (status) {
+ if (WIFSIGNALED(status))
+ eprintf("subprocess git was killed by signal %i\n", WTERMSIG(status));
+ exit(libsimple_default_failure_exit);
+ }
+ if (dirsize - dirlen < sizeof("/.git"))
+ dir = erealloc(dir, dirlen + sizeof("/.git"));
+ stpcpy(&dir[dirlen], "/.git");
+
+ if (mount(dir, dir, NULL, MS_BIND, NULL))
+ eprintf("mount %s %s NULL MS_BIND NULL:", dir, dir);
+ if (mount("none", dir, NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL))
+ eprintf("mount none %s NULL MS_BIND|MS_REMOUNT|MS_RDONLY NULL:", dir);
+
+ free(dir);
+
+ if (setegid(getgid()))
+ eprintf("setegid <real group>:");
+ if (seteuid(getuid()))
+ eprintf("seteuid <real user>:");
+
+ execvp(argv[0], argv);
+ enprintf(errno == ENOENT ? 127 : 126, "execvp %s:", argv[0]);
+ return 0;
+}