Version Control and Developer Tooling Questions
The everyday toolchain of software work: version control with Git (branching, merging, rebasing, conflict resolution, using git bisect to find a regression), command-line and shell proficiency for day-to-day navigation, log inspection, and troubleshooting, IDE and editor workflows, build systems and package/dependency management (npm, Maven, pip, Gradle, CocoaPods, and embedded/cross-compilation toolchains), and the growing practice of AI-assisted coding: using, reviewing, and verifying AI-generated code and tests. Deliberately generic across languages and stacks; language- and domain-specific frameworks live in their own categories. This topic covers a developer's individual command of these tools, not: writing durable shell automation and glue scripts (Shell Scripting and Automation owns that), producing, versioning, and publishing build artifacts or container images (Build Automation and Artifact Management owns that), release cadence and change governance (Release Management and Change Control owns that), or diagnosing a live production incident end to end (Performance Troubleshooting and Incident Response and the Observability topics own that).
As a staff-level MLE, how would you set team standards for AI-assisted coding reviews so engineers can move quickly without normalizing low-quality or unsafe code?
Sample Answer
Direct answer
As a staff-level MLE (machine learning engineer, a senior individual-contributor role expected to set technical direction across a team), I would set standards that make AI assistance safe by default and visible by design, rather than trying to police every individual use after the fact. The core idea is that a reviewer must always be able to explain, in plain language, what a change does and why it is safe, regardless of who or what wrote the first draft.
Structured elaboration
Team standards
- AI-generated code is labeled as such in the pull request description, so review effort can be calibrated appropriately rather than assumed.
- Every non-trivial change ships with tests, documentation updates where relevant, and a short human explanation of intent in the PR, not just a diff.
- Public interfaces, training pipelines, and serving logic get a stricter review bar than internal utilities, since the blast radius of a mistake differs enormously between them.
- Nothing merges if the reviewer cannot explain the change back in plain language. If a reviewer cannot restate what a change does and why it is safe, that is a signal to slow down, not a formality to skip.
Review quality bar
- Review for correctness, security, and maintainability, not just style or formatting, since AI-generated code tends to be stylistically clean even when it is substantively wrong.
- Ask explicitly: does this preserve existing behavior, is the change observable (will we know if it breaks), and can it be rolled back quickly if it does?
- Favor small pull requests, since an AI-assisted mistake buried in a large diff is far harder to isolate than one in a five-line change.
Guardrails for a regulated system
For a regulated system that handles customer decisions (credit, eligibility, anything with a compliance or audit obligation attached), the standards above are necessary but not sufficient. I would add: a documented audit trail for every AI-assisted change touching the decisioning path, tying the prompt, the generated output, and the human review together, not just the final diff; a requirement that any change to decision logic includes an explanation of impact on protected classes or fairness metrics where applicable, reviewed by someone with the authority to block on that basis; and stricter sign-off, meaning a compliance-aware reviewer, not just any available engineer, for anything touching the regulated decision path specifically, distinct from the general engineering review everything else gets.
Enforcing this mechanically, not just by policy
A written standard nobody enforces is not a standard, it is a suggestion. I would wire the review bar into CI (continuous integration, the automated pipeline that runs checks on every change) as gates that must pass before merge is even possible: linting and formatting, the full unit test suite, static analysis for common unsafe patterns (unsafe deserialization, missing input validation, hardcoded credentials), and a small model-quality smoke test for anything touching training or serving code, comparing a quick evaluation run against a known baseline so an obviously broken model cannot merge silently. The point of putting this in CI rather than relying on reviewer diligence alone is that it catches the same class of mistake every time, without depending on which reviewer happened to be paying close attention that day.
Enablement, not just enforcement
- Build a small internal library of examples: good AI-assisted diffs next to anti-patterns that looked fine but were not, so engineers have a concrete reference rather than an abstract policy document.
- Teach engineers to prompt for constraints (explicit interfaces, explicit edge-case handling), not just for a working solution, since a prompt that only asks for "make this work" tends to get exactly that and nothing more.
Worked example
This is an illustrative story, not a reported metric.
A team building a credit-decisioning feature used an AI assistant to draft a new eligibility rule. The pull request was labeled AI-assisted per the team standard, which is what prompted the reviewer to read every line instead of skimming a diff that "looked clean." The reviewer noticed the generated code used a column, applicant_zip_code, that the team had already agreed to exclude from decisioning elsewhere in the codebase because it correlates with protected-class information. Because the standard required a compliance-aware reviewer's sign-off on anything touching the decisioning path, that reviewer caught it before merge, not months later during a fairness audit. The fix: the feature was reworked to drop the column, a static-analysis rule was added to the CI gate that flags any new column reference inside the decisioning module against a maintained deny-list, and the incident became one of the concrete anti-pattern examples in the team's internal AI-assisted-code library.
Over time, in aggregate, not from one story, the signals I would watch for are: fewer post-merge regressions traced back to AI-assisted changes relative to human-only changes, review turnaround time staying roughly stable rather than either exploding (too much friction) or collapsing (rubber-stamping), and CI gate failures shifting toward being caught earlier, which together suggest the standards are actually changing behavior rather than just adding a checkbox nobody reads.
Trade-offs and pitfalls
Standards that are too heavy get quietly bypassed, and standards that are too light normalize exactly the risk they were meant to prevent. The right calibration is proportional to blast radius: a one-line internal utility change does not need the same ceremony as a change to the regulated decisioning path, and treating them identically either slows the team to a crawl on low-risk work or, more dangerously, trains people to treat the heavy process as boilerplate they route around on high-risk work too.
Explain the difference between git clone, git fetch, git pull and git push. In what scenarios would you use fetch + merge vs pull --rebase? Describe risks and common pitfalls for a collaborative team.
Sample Answer
Direct answer
git clone is a one-time operation that copies an entire remote repository (all branches and history) to create a new local repository. git fetch downloads new commits and branches from a remote into your local repo's remote-tracking branches (like origin/main) without touching any of your own local branches. git pull is git fetch followed by automatically integrating the fetched branch into your current local branch, by default via a merge, but configurable to rebase instead. git push uploads your local commits to update a branch on the remote.
Structured elaboration
fetch + merge vs. pull --rebase. Plain git pull (the default) fetches, then merges origin/main into your local main, creating a merge commit if the two have diverged. git pull --rebase fetches, then replays your local commits on top of the freshly-fetched remote branch instead, producing a linear history with no merge commit.
Use --rebase when you want a clean, linear history for commits nobody else has based work on yet, it reads better in git log later. Use a plain merge (or git fetch followed by an explicit git merge) when you want the history to honestly reflect that two lines of work happened concurrently, or, importantly, when the commits you're integrating have already been pushed and shared, since rebasing shared commits carries the same shared-history risk as any other rebase (see below).
Worked example
Two developers both start from the same origin/main. Developer A commits locally and pushes. Developer B, unaware, also commits locally, then runs git pull. Since origin/main now has A's commit that B's local main doesn't, git creates a merge commit joining both histories: git log --graph on B's machine shows a fork-and-join shape. If B had instead run git pull --rebase, git would have fetched A's commit, then replayed B's local commit on top of it, giving a single straight line with B's commit last, no merge commit at all, same end content, different history shape.
Trade-offs and pitfalls
For a collaborative team specifically: defaulting everyone to plain git pull means merge commits accumulate constantly on a busy shared branch, which clutters git log with noise that has no real informational value beyond "two people worked on this around the same time." Defaulting everyone to git pull --rebase is fine for commits that are still only local, but is dangerous the moment those commits have already been pushed and someone else has pulled them, rebasing rewrites their hashes, and a subsequent force-push causes the exact same shared-history problem as any other rebase of shared work: collaborators' next pull either duplicates commits or conflicts outright. The safest team-wide default is usually to pick one convention explicitly and configure it (git config pull.rebase true sets rebase as the repo-wide default) rather than leaving it to each developer's local git config, an inconsistent mix of merge-pullers and rebase-pullers produces a history that's neither clean nor honestly concurrent, just inconsistent.
Describe how to implement client-side and server-side Git hooks to block commits that contain secrets (passwords/API keys). Include examples of tools or libraries you would use, where the hooks run, and how you would handle false positives to avoid developer friction.
Sample Answer
Direct answer
I would use two layers: a fast client-side pre-commit hook for immediate developer feedback, and an authoritative server-side check that actually blocks the push or merge, because a client-side hook alone is not a security boundary. A Git hook is just a script Git runs automatically at a point in its workflow (before a commit, before a push accepted by the server, and so on); anything that only runs on the developer's own machine can be skipped, so the enforcement that matters has to happen where the developer does not control the environment.
Structured elaboration
Client-side layer (fast feedback, not enforcement)
- Use the
pre-commitframework (pre-commit.com): the repo carries a versioned.pre-commit-config.yaml, and each developer runspre-commit installonce to wire a real hook into their local.git/hooks/pre-commit. - Plug in a secret-scanning tool as a hook:
gitleaksordetect-secrets(from Yelp) both scan the staged diff for patterns (AWS-style keys, private key headers) and high-entropy strings (a long, random-looking sequence of characters is a decent proxy for "this looks like a real secret, not a word"). - Example config:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- Scope: scan only the staged diff, not the whole tree, so it stays fast (seconds, not minutes) and does not annoy developers on every commit.
Server-side layer (the real enforcement point)
- On a self-managed Git server (GitHub Enterprise Server, GitLab self-managed, Bitbucket Server), you can install a real
pre-receivehook: a script the server runs on every incoming push, before any ref is updated, that can reject the push outright. - On hosted SaaS (github.com, gitlab.com) you generally cannot install a custom pre-receive hook on an ordinary repo. The equivalent there is (a) a required, blocking CI check that runs the same scanner against the pushed commits before a merge is allowed, and (b) the platform's own secret-scanning push protection (GitHub Advanced Security's secret scanning, for example, intercepts the push server-side when it recognizes a known secret pattern).
- The point of the server-side layer is that it does not care whether the developer had the client hook installed, disabled it, or ran
git commit --no-verifyto skip it.
Handling false positives without training people to bypass the tool
- Maintain a baseline/allowlist file (
detect-secretscalls this.secrets.baseline) for known-safe matches, like test fixtures that intentionally contain fake keys. - Scope detectors to file types and paths most likely to hold secrets, and tune entropy thresholds so ordinary hashes, UUIDs, and encoded blobs stop tripping the scanner.
- Give a documented override path: a reviewer-approved bypass tied to a ticket, logged, rather than a silent skip, so an override is visible and rare instead of a habit.
- Track the false-positive rate as a metric. A noisy hook is worse than no hook, because it teaches developers to reflexively bypass anything red.
Worked example
A developer edits config.py and accidentally leaves a real value staged: AWS_SECRET_ACCESS_KEY = "AKIA..."). On git commit, the pre-commit hook's entropy and pattern rules flag the exact file and line, and the commit is aborted with a message pointing at it. If the developer bypasses that with git commit --no-verify (or never ran pre-commit install in the first place) and pushes, the server-side layer is the backstop: on a self-managed server the pre-receive hook rejects the push before the branch updates; on GitHub.com, the required CI job fails the pull request and push protection can block the push itself if it recognizes the secret's format. Either way, the secret never reaches a branch other reviewers can pull.
Trade-offs and pitfalls
Client-side hooks improve turnaround time but are fundamentally advisory, not authoritative, since they run in an environment the developer controls. Server-side checks are authoritative but add latency to CI. Detector aggressiveness is a real dial: too strict burns trust and gets bypassed, too loose lets real secrets through. Finally, catching a secret in a new commit does not undo exposure that already happened: if a real credential is found even after the fact, the fix is rotating the credential and, separately, purging it from history, not just relying on the hook going forward.
Implement a POSIX shell pre-commit hook (script saved at .git/hooks/pre-commit) that scans all staged files for trailing whitespace and refuses the commit if any is found. Include installation instructions (how to make the hook executable) and describe how you'd extend it to skip binary files and run fast on large repos.
Sample Answer
Direct answer
Write a POSIX sh script at .git/hooks/pre-commit that inspects the staged (index) version of each changed file for trailing whitespace, and exits non-zero to block the commit if it finds any, or 0 to allow it.
Structured elaboration
#!/bin/sh
# .git/hooks/pre-commit
# Reject commits that introduce trailing whitespace in staged files.
offenders=""
for f in $(git diff --cached --name-only --diff-filter=ACM); do
# skip binary files: test the STAGED CONTENT itself for binary-ness, not
# the diff summary (which is always plain text even for a binary file, so
# testing it here can never actually detect one).
if ! git show ":$f" | grep -Iq . ; then
continue
fi
if git show ":$f" | grep -nE ' +$' > /tmp/tw_hits.$$; then
if [ -s /tmp/tw_hits.$$ ]; then
offenders="$offenders $f"
fi
fi
rm -f /tmp/tw_hits.$$
done
if [ -n "$offenders" ]; then
echo "pre-commit: trailing whitespace found in:$offenders"
echo "Fix with your editor's trim-on-save, or: sed -i 's/[ \t]*$//' <file>"
exit 1
fi
exit 0
Install it:
chmod +x .git/hooks/pre-commit
Git only runs hooks that are executable; without chmod +x the file sits there silently doing nothing, no error, which is a common first-try trap.
Key points:
git diff --cached --name-only --diff-filter=ACM: lists the STAGED files, filtered to Added, Copied, Modified, so a deleted file is not checked against content that no longer exists.git show ":$f": reads the file's content as it exists in the index (staged), not on disk. That matters if you staged part of a file and then made further unstaged edits, the hook should judge what is actually about to be committed.grep -Iq .: a quick binary-file detector;-Imakes grep treat binaries as non-matching, so this check answers "does this file have any text content at all." It has to run ongit show ":$f", the actual staged blob, not ongit diff --cached -- "$f": git's diff output for a binary file is itself the plain-text lineBinary files a/... and b/... differ, so piping the diff throughgrep -Iq .always finds text and would never skip anything, that was the bug in an earlier version of this hook until I actually staged a binary file and watched it get flagged.grep -nE ' +$': finds lines ending in one or more spaces (trailing whitespace);-nprints line numbers.
Worked example
$ git init -q && cd repo1
$ printf 'line one\nline two \n' > bad.txt # trailing spaces on line 2
$ git add bad.txt
$ git commit -m "test bad"
pre-commit: trailing whitespace found in: bad.txt
Fix with your editor's trim-on-save, or: sed -i 's/[ \t]*$//' <file>
$ echo $?
1
$ printf 'line one\nline two\n' > bad.txt # whitespace removed
$ git add bad.txt
$ git commit -m "test good"
[main (root-commit) 4aa9153] test good
1 file changed, 2 insertions(+)
$ echo $?
0
This was run exactly as shown: the first commit is blocked and exits 1, the second, after fixing the whitespace, succeeds and exits 0.
A third case exercises the binary-skip path specifically, since that's the part most likely to silently regress:
$ head -c 200 /dev/urandom > bin.dat
$ git add bin.dat
$ git commit -m "add random binary"
[main eaeaa2f] add random binary
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 bin.dat
$ echo $?
0
A random 200-byte binary blob commits cleanly, exit 0, confirming the skip check is reading the staged content itself (which contains NUL bytes) rather than the always-textual diff summary.
Complexity & edge cases
- Complexity: one
git diffto list staged files, then onegreppass per staged file, proportional to the number of staged files times their size. Fine for a normal commit; a commit that stages an unusually large generated file would benefit from a size cutoff before running the whitespace scan on it. - Skipping binary files is what the
grep -Iq .check does, but only when it's run against the staged blob content (git show ":$f"); without it, or if it's accidentally run against the diff summary instead of the content, a staged binary can trip a false positive trying to text-match inside it. - Running fast on large repos: this hook only ever looks at STAGED files, not the whole repository, so its cost scales with the size of a single commit, not with repo size, that is inherent to the design. A separate, heavier full-repo scan run as a CI gate is a different concern from the local hook's speed.
- Hooks live in
.git/hooks/, which is NOT tracked by git itself (it lives inside the local.gitdirectory), so this script must be distributed separately, either through a setup script that copies or symlinks it into place during onboarding, or a framework likepre-commitorhuskythat manages hook installation and versions the hook definitions inside the repository proper.
Describe the roles and relationships of the working directory, the staging area (index), the local repository (commits), and a remote repository in Git. Provide a short example workflow showing the commands you would run to move a file from edit to a pushed commit on origin.
Sample Answer
Direct answer
Git tracks a change through four places: the working directory (the actual files on disk that you edit), the staging area (also called the index, a snapshot you build up with git add of exactly what will go into the next commit), the local repository (the committed history stored in .git, on your machine only), and a remote (another copy of the repository, for example on GitHub, that your local repo can sync with). A change moves through these in order: edit a file, stage it, commit it, push it.
Structured elaboration
- Working directory: whatever is currently on disk. A newly-created or edited file here is "untracked" or "modified" until you tell git to notice it.
- Staging area (index): a snapshot you explicitly build with
git add <file>. It exists so you can commit exactly the changes you intend, even if your working directory has other, unrelated edits you're not ready to commit yet. - Local repository: the committed history.
git committakes whatever is currently staged and turns it into a permanent, addressable commit in.git, clearing the staging area in the process. This is entirely local, nobody else can see it yet. - Remote: a separate copy of the repository elsewhere (GitHub, GitLab, a teammate's machine).
git pushuploads your local commits to update a branch there;git fetch/git pullbring a remote's commits down to you.
Worked example
echo "hello" > greeting.txt # working directory: an untracked file
git add greeting.txt # staging area: a snapshot ready to commit
git commit -m "add greeting" # local repository: a new commit, staging area is now clear
git push origin main # remote: the commit is uploaded to origin/main
After the first command, git status reports greeting.txt as untracked. After git add, it reports it as staged. After git commit, git status shows a clean working tree, but the commit exists only in your local repository, git log shows it, but nobody else can see it yet. Only after git push does the commit exist on origin for others to fetch.
Trade-offs and pitfalls
The most common mix-up, especially early on, is treating "committed" and "pushed" as the same thing, a commit is fully real and permanent in your local history the moment you run git commit, but it's invisible to everyone else until you push it. Another common trip-up: git add stages the file's content at that moment, if you edit the file again after staging it, the new edits are back in the working directory and won't be included in the commit unless you git add again. git status is the tool for seeing exactly what's in each of these states at any point, it's worth running constantly rather than guessing.
Unlock Full Question Bank
Get access to all Version Control and Developer Tooling interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.