Monsieur Ganesha
Pre-commit hooks for the 42 school piscine — norminette, compiler, forbidden functions, commit format, README check.
v0.1.0 · Python 3.11+ · MIT
What is it?
Monsieur Ganesha is a suite of five
pre-commit hooks that run
automatically before every git commit in a 42 piscine
repository. The hooks catch norm violations, compile errors, forbidden
function calls, malformed commit messages, and missing README structure
before they reach the repository — so moulinette never has
to see them first.
| Hook id | What it checks | Tool |
|---|---|---|
norminette |
42 Norme compliance on .c and .h files |
norminette CLI |
c-compiler |
Syntax errors in staged .c files |
gcc -fsyntax-only |
forbidden-functions |
Calls to functions banned by the subject PDF | Pure-Python regex |
commit-message |
Commit subject format (Conventional Commits 1.0.0) | Pure-Python regex |
readme |
README.md structure — non-blocking advisory (+XP) | Pure-Python |
The characters
Bonjour, monde.
I am not here to be gentle.
I am here so she does not have to be harsh. — Monsieur Ganesha, Directeur, Conservatoire de Paris XLII
Named after Monsieur Ganesha, director of the Conservatoire de Paris XLII and husband of Mademoiselle Norminette.
Mademoiselle Norminette and her younger sister, Mademoiselle Francinette, are the daughters of the respectable Madame Moulinette. The rigor is the care. The demand is the protection. Madame Moulinette has no mercy; Mademoiselle Norminette, even less — but Ganesha at least has a reason.
Monsieur Ganesha was the first student of Paris XLII to complete every exercise list and exam with honours — using a single push per step. Madame Moulinette found this absurd. Ganesha recalls: c'était terrible.
— Monsieur Ganesha, in confidence
Requirements
- Python 3.11+ (uses
tomllibfrom stdlib) - pre-commit ≥ 3.0
- norminette — for the
norminettehook:pip install norminette - gcc — for the
c-compilerhook:sudo apt install gcc
Installation
Automatic (recommended)
Run the setup script from inside your piscine repository:
bash /path/to/monsieur-ganesha/install.sh
The script:
- Sets
git config --global core.editor vim - Installs
pre-commitviauvorpip3 - Creates
.pre-commit-config.yamlpointing to this repository - Creates a
.ganesha.tomltemplate with sensible defaults - Activates the hooks with
pre-commit install
Manual
Add the following to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/qlrd/monsieur-ganesha
rev: v0.1.0
hooks:
- id: norminette
- id: c-compiler
- id: forbidden-functions
- id: commit-message
Then activate the hooks:
pre-commit install
pre-commit install --hook-type commit-msg
git commit is called without -m.
vim is the expected editor at 42 school.
Run git config --global core.editor vim to set it.
Configuration
Place .ganesha.toml at the root of your piscine
repository. All sections are optional; the hooks work without a
configuration file.
[project]
# Identifier for the current module (informational only).
name = "C00"
[forbidden]
# Functions disallowed by the subject PDF.
# The hook rejects any .c file that calls one of these.
functions = ["printf", "malloc", "realloc", "free", "calloc"]
[commit]
# Override the default Conventional Commits 1.0.0 pattern.
# Uncomment to use the 42-school ex00: format instead.
# pattern = "^(ex|rush|exam)\\d+: .+"
Configuration reference
| Key | Type | Default | Description |
|---|---|---|---|
[project] name |
string | none | Module identifier. Informational only. |
[forbidden] functions |
string[] | [] |
C function names to forbid. Empty = hook always passes. |
[commit] pattern |
string (regex) | CC 1.0.0 pattern | Custom regex for the commit subject. Disables gamification. |
Hooks
norminette
Runs the norminette CLI on every staged
.c and .h file. Norminette enforces the
42 Norme —
a strict coding standard covering indentation, line length,
function length, variable placement, and more.
# .pre-commit-config.yaml
- id: norminette
If norminette is not installed the hook prints an installation hint
and exits with code 1. Install with:
pip install norminette
c-compiler
Runs gcc -Wall -Wextra -Werror -fsyntax-only on each
staged .c file independently. The
-fsyntax-only flag skips object-file generation,
which avoids conflicts when multiple files are staged and some
headers are not yet available.
# .pre-commit-config.yaml
- id: c-compiler
forbidden-functions
Scans every staged .c file for calls to functions
listed in [forbidden] functions.
Detection is word-boundary anchored:
ft_printf is not flagged when
printf is forbidden.
# .pre-commit-config.yaml
- id: forbidden-functions
Error output format:
ex00/ft_putchar.c:8: função proibida 'write'
// …) are stripped before
scanning. Multi-line /* */ comments are not stripped
— known limitation.
commit-message
Validates the commit message subject against
Conventional Commits
1.0.0. Git passes .git/COMMIT_EDITMSG to the hook;
comment lines (starting with #) are stripped before
validation.
# .pre-commit-config.yaml
- id: commit-message
Rejection output is intentionally terse — REJECTED. —
with no format hints. Students are expected to discover the correct
format themselves.
readme
Checks every staged README.md for common structural
issues. This hook is non-blocking: it always exits
with code 0 and never prevents a commit. Files with issues
receive advisory messages; files that pass all checks earn
+XP.
# .pre-commit-config.yaml
- id: readme
Advisory messages
When fully documented
When README exists but is not staged
The hook always runs (always_run: true) and checks only the
top-level README.md. A level-1 ATX heading is a line beginning
with # (hash followed by a space). File descriptor usage is
detected by scanning for stdin, stdout,
stderr, or fd in the file body.
Conventional Commits 1.0.0
The commit subject must follow the pattern:
<type>[(<scope>)][!]: <description>
- type — one of the allowed types (see below)
- scope — optional, parenthesised, e.g.
(cli) - ! — optional breaking-change marker
- description — must start with a non-whitespace character
The subject line must not exceed 72 characters.
Valid examples
feat: add norminette timeout option
fix(forbidden): handle empty file list
feat!: drop support for gcc older than 12
fix(compiler)!: change exit code on parse error
docs: update installation section
test: add integration test for commit-msg
init: project setup
Invalid examples
WIP # no type prefix
ex00: implement ft_putchar # 42-school format, not CC 1.0.0
FEAT: something # type must be lowercase
feat: # missing description
feat: <73 chars...> # subject too long
Type reference
| Type | When to use | Gamification |
|---|---|---|
| feat | A new feature | — |
| fix | A bug fix | — |
| docs | Documentation only | francinette approves |
| style | Formatting, whitespace — no logic change | — |
| refactor | Code restructuring — no feature, no fix | — |
| perf | Performance improvement | — |
| test | Adding or correcting tests | ultimate type easter egg |
| build | Build system, dependencies | — |
| ci | CI/CD configuration | — |
| chore | Maintenance tasks — no production code change | francinette approves |
| revert | Reverts a previous commit | — |
| init | Initial commit / project bootstrap | — |
Gamification layer
When the default Conventional Commits pattern is active, accepted commits trigger informational messages on stderr:
| Condition | Message |
|---|---|
docs or chore commit |
francinette approves this docs commit. |
test commit |
you discovered the ultimate commit type. |
Breaking change (!) with body |
breaking change explained — +XP. |
Breaking change (!) without body |
tip: explain why this breaks to earn +XP. |
These messages are purely informational. They do not affect the hook exit code and are not shown when a custom pattern is configured.
CLI reference
After installation with uv sync or
pip install -e . the ganesha command
is available on $PATH. Each subcommand mirrors a hook.
Exit code is 0 on success, 1 on failure,
2 on internal error.
ganesha norminette <files...>
Run norminette on the given files.
ganesha norminette ex00/ft_putchar.c include/header.h
ganesha compiler <files...>
Syntax-check the given files with gcc.
ganesha compiler ex00/ft_putchar.c ex01/ft_print_alphabet.c
ganesha forbidden <files...>
Scan the given files for forbidden function calls.
Reads [forbidden] functions from
.ganesha.toml in the current directory.
ganesha forbidden ex00/ft_putchar.c
ganesha commit-msg <file>
Validate the commit message stored in file.
Reads the optional custom pattern from
[commit] pattern in .ganesha.toml.
ganesha commit-msg .git/COMMIT_EDITMSG
ganesha readme <files...>
Check the given README.md files for structural issues.
Always exits 0. Advisory messages are printed to
stderr.
ganesha readme README.md
API reference
All public functions can be imported and called directly, which is useful for testing or for embedding Monsieur Ganesha checks into other tools.
ganesha.config
load_config(root: Path | None = None) → Config
Load .ganesha.toml from root (defaults to
the current working directory). Returns a default
Config if the file does not exist. Raises
ValueError if the file exists but contains invalid
TOML.
Returns: Config — fully-populated configuration
dataclass with safe defaults.
from ganesha.config import load_config
cfg = load_config()
print(cfg.forbidden.functions) # ['printf', 'malloc']
print(cfg.commit.pattern) # None (use default CC 1.0.0)
Dataclasses
| Class | Fields | Defaults |
|---|---|---|
Config |
project, forbidden, commit |
nested defaults |
ProjectConfig |
name: str | None |
None |
ForbiddenConfig |
functions: list[str] |
[] |
CommitConfig |
pattern: str | None |
None |
ganesha.checks.norminette
check(files: Sequence[str]) → bool
Run norminette on all .c and
.h files in files. Non-matching files are
ignored. Returns True if norminette exits with
code 0, False otherwise. Returns
True immediately if no matching files are present.
from ganesha.checks.norminette import check
ok = check(["ex00/ft_putchar.c", "include/libft.h"])
ganesha.checks.compiler
check(files: Sequence[str]) → bool
Run gcc -Wall -Wextra -Werror -fsyntax-only on each
.c file in files (one subprocess per file).
All failures are collected before returning. Returns
False if gcc is not installed.
from ganesha.checks.compiler import check
ok = check(["ex00/ft_putchar.c", "ex01/ft_print_alphabet.c"])
ganesha.checks.forbidden
check(files: Sequence[str], forbidden: Sequence[str]) → bool
Scan .c files for calls to any function in
forbidden. Uses the regex
\b(f1|f2|...)\s*\( so that
ft_printf is not flagged when printf
is forbidden. Returns True if forbidden
is empty.
from ganesha.checks.forbidden import check
ok = check(["ex00/ft_putchar.c"], ["printf", "malloc"])
ganesha.checks.commit_msg
Constants
DEFAULT_PATTERN |
Conventional Commits 1.0.0 regex string |
MAX_SUBJECT_LEN |
72 — maximum subject line length in characters |
check(file_path: str, pattern: str = DEFAULT_PATTERN) → bool
Validate the commit message at file_path.
Strips # comment lines, checks subject length, then
matches against pattern. When using
DEFAULT_PATTERN the gamification layer is active.
All rejection output goes to stderr; nothing is raised.
from ganesha.checks.commit_msg import check, DEFAULT_PATTERN
ok = check(".git/COMMIT_EDITMSG")
# or with a custom pattern:
ok = check(path, pattern=r"^(ex|rush|exam)\d+: .+")
ganesha.checks.readme
check(files: Sequence[str]) → bool
Check each README.md in files for structural
issues: non-empty, level-1 ATX heading, file descriptor usage.
Advisory messages are printed to stderr with
⚠ / ✓ prefixes. Always returns
True (non-blocking).
When called with no files (files is empty) and a
top-level README.md exists on disk, prints a staging
reminder and returns True.
from ganesha.checks.readme import check
ok = check(["README.md"]) # always True
Development
Setup
Python 3.11+ and uv are required.
git clone https://github.com/qlrd/monsieur-ganesha
cd monsieur-ganesha
uv sync --dev
Quality gates
All of the following must pass before committing:
black --check src/ tests/ # formatting (88-column)
isort --check-only src/ tests/ # import order
pylint src/ganesha/ # lint — 10.00/10 required
pytest # all 45 tests must pass
To auto-fix formatting before checking:
black src/ tests/
isort src/ tests/
Project layout
src/ganesha/
├── __init__.py public API (re-exports checks + config)
├── __main__.py python -m ganesha entry point
├── cli.py CLI entry point (argparse)
├── config.py .ganesha.toml loader (tomllib)
└── checks/
├── __init__.py
├── norminette.py subprocess: norminette
├── compiler.py subprocess: gcc -fsyntax-only
├── forbidden.py pure-Python regex scan
├── commit_msg.py CC 1.0.0 validator + gamification
└── readme.py README structure check (non-blocking)
tests/
├── fixtures/ valid.c · norm_error.c · compile_error.c · forbidden.c
├── test_forbidden.py integration via lib API + tmp_path
├── test_commit_msg.py integration via lib API + tmp_path
└── test_cli.py CLI integration via subprocess
Contributing
All contributions require a
Developer Certificate of
Origin. Add it with git commit -s.
Commit messages must follow Conventional Commits 1.0.0.
For large changes, open an issue before starting work. See CONTRIBUTING.md for the full workflow.
Adding a new check
- Create
src/ganesha/checks/<name>.pywith a publiccheck()function and verbose docstrings. - Export it in
checks/__init__.py. - Add a CLI subcommand in
cli.py. - Add an entry in
.pre-commit-hooks.yaml. - Write integration tests in
tests/test_<name>.py. - Run all quality gates and open a pull request.
Rebase note
Congratulations.
If you are reading this, you have completed at least one correct rebase — withfixup,reword, oredit— and the reflog proves it. The reflog has not been modified. It never should be. You rewrote with intention, and the reflog remembers.
git reflogdoes not lie. It shows every move you made. A clean rebase, visible in the reflog, is a signature. It says: I understood what I was doing.
Monsieur Ganesha has noted this. — Monsieur Ganesha, Directeur, Conservatoire de Paris XLII
The safest rebase workflow — without overwriting reflogs:
git reflog # read history — never modify it
git rebase -i HEAD~3 # fixup, reword, edit — with intention
git reflog # find the sha before the rebase
git reset --hard HEAD@{n} # recover the exact state if needed
Never modify the reflog. Never use git push --force
to overwrite a shared branch. The reflog on your machine is yours —
and it is the only honest witness you have.
Public keys
All commits are signed by
Monsieur Piscinette
(piscinette@conservatoire42.fr) and attested by the
Mahatrimurti: Madame Kali, Monsieur Shiva, and Monsieur Ganesha.
Import all keys to verify signatures locally:
gpg --import keys/*.asc
git log --show-signature
| File | Identity | Usage |
|---|---|---|
keys/madame-kali.asc | Madame Kali | Root of trust [C] |
keys/monsieur-shiva.asc | Monsieur Shiva | Consciousness, purge support [C] |
keys/monsieur-ganesha.asc | Monsieur Ganesha | Directeur du Conservatoire [C] |
keys/bija-krim.asc | Bija Krim (Krīṃ) | Seed mantra of Kali [C] |
keys/monsieur-piscinette.asc | Monsieur Piscinette | Commit signer [SC] |
keys/qlrddev.asc | qlrddev@proton.me | Maintainer personal key [S] |