DEV Community

Cover image for Day 34: A Hook That Cannot Fail, and an Error Three Commands From Its Cause
Nnamdi Felix Ibe
Nnamdi Felix Ibe

Posted on AI-assisted

Day 34: A Hook That Cannot Fail, and an Error Three Commands From Its Cause

Today's theme is failures that report themselves somewhere other than where they happened. A Git hook with the wrong permissions does nothing and says nothing. A heredoc with a trailing space produces an error about a missing zip file, three commands downstream.

One Git task, one AWS task. Install a post-update hook that tags every push to master, then deploy a Python Lambda from the CLI. The tasks come from the KodeKloud Engineer platform.

This is also the last Git task in the track. Days 21 to 34 covered the lot: repository setup, forks, branches, merges, remotes, revert, cherry-pick, pull requests, hard reset, stash, rebase, conflicts, and now hooks.

The hook that runs after it is too late to object

The task was to make every push to master create a release tag automatically. That work belongs on the server, in the bare repository:

cd /opt/apps.git/hooks
vi post-update
Enter fullscreen mode Exit fullscreen mode
#!/bin/bash

for ref in "$@"
do
    if [ "$ref" = "refs/heads/master" ]; then
        TODAY=$(date +%F)
        COMMIT=$(git rev-parse refs/heads/master)
        git tag "release-$TODAY" "$COMMIT" 2>/dev/null
    fi
done
Enter fullscreen mode Exit fullscreen mode
chmod +x post-update
Enter fullscreen mode Exit fullscreen mode

post-update receives the name of every ref that was just updated, one per argument, which is what "$@" is walking. Two details in that script are worth more than they look. git rev-parse needs no repository path because GIT_DIR is already set when a hook runs. And redirecting the tag error to /dev/null makes the hook idempotent for the day: the second push leaves the existing tag alone instead of failing loudly.

Then the part that decides whether any of this is a good idea. Git's documentation says post-update is meant primarily for notification and cannot affect the outcome of git receive-pack. It runs after the refs have already moved. Exit non-zero, and the push still succeeds.

So if you want a hook that rejects bad pushes, this is the wrong hook. pre-receive and update run before refs move and can fail the push. post-receive and post-update run afterwards and cannot. Pick the wrong one, and you have written a validation rule that logs its objection into the void while the push lands anyway.

And the quiet one:

chmod +x post-update
ls -la | grep post-update
Enter fullscreen mode Exit fullscreen mode

A hook file with perfect contents and no execute bit is skipped in silence. Nothing in the push output mentions it. This is also why Git ships its samples with a .sample suffix, and why hooks never travel: they live in the repository directory, and neither clone nor push carries them. A hook that has to run on the server has to be put on the server.

One last thing, because it looks like the hook failed when it did not:

git fetch --tags
git tag
# release-2026-08-23
Enter fullscreen mode Exit fullscreen mode

The tag was created on the server. Your clone does not know about it until you ask.

The trailing space

The AWS half was Day 33's Lambda again, with a different name and a pre-existing role.

Two useful things before the mistake. aws iam get-role before create-role, because this lab pre-provisions the role and create-role is not idempotent, so a blind create returns EntityAlreadyExists and aborts a chained script. Read before write when a resource might already exist. Worth knowing that attach-role-policy is idempotent, so re-attaching a policy is safe.

Now the mistake:

cat > /root/lambda-build/lambda_function.py <<'EOF'
def lambda_handler(event, context):
    ...
EOF 
Enter fullscreen mode Exit fullscreen mode

There is a space after that final EOF. The shell never recognises its terminator, keeps reading, and drops the prompt to >. Ctrl+C out and the file was never written.

Three commands later:

Unable to load paramfile fileb:///root/function.zip: [Errno 2] No such file or directory
Enter fullscreen mode Exit fullscreen mode

The zip step had been skipped because the source did not exist, so the zip did not exist, so create-function failed. The error names a missing archive. The cause is an invisible character in a command that appeared to succeed three steps earlier.

The fix I have adopted since is to stop pasting heredocs for short files:

printf '%s\n' \
  'def lambda_handler(event, context):' \
  '    return {' \
  '        "statusCode": 200,' \
  '        "body": "Welcome to KKE AWS Labs!"' \
  '    }' \
  > /root/lambda-build/lambda_function.py
Enter fullscreen mode Exit fullscreen mode

Each argument becomes a line, single quotes prevent expansion, and there is no terminator to get wrong. Then cat the file rather than assuming.

The same principle applies to the artifact:

cd /root/lambda-build && zip -q /root/function.zip lambda_function.py && unzip -l /root/function.zip
Enter fullscreen mode Exit fullscreen mode
  Length      Date    Time    Name
      125  2026-08-25 07:44   lambda_function.py
Enter fullscreen mode Exit fullscreen mode

The name column has no directory prefix, which is the only thing that matters. lambda-build/lambda_function.py inside the archive would deploy fine and fail at invoke time with an import error. Check the artifact, not the exit code of the tool that made it.

And one habit worth stealing outright:

aws lambda wait function-active --region us-east-1 --function-name datacenter-lambda-cli
Enter fullscreen mode Exit fullscreen mode

create-function returns with "State": "Pending" and invoking a pending function fails. There is a waiter for that, and there are waiters for far more things than most people expect: ec2 wait instance-running, rds wait db-instance-available, cloudformation wait stack-create-complete. Any sleep 30 in a script is worth one search.

Look upstream of the error

The hook and the heredoc are the same problem wearing different clothes. In both cases, the thing that broke was silent, and the thing that spoke up was downstream of it and blameless.

The habit that helps is separating "this command exited zero" from "this command did what I wanted", and checking the second one deliberately: ls -la on the hook, unzip -l on the archive, git fetch --tags before concluding the tag was never made.

So here is the Day 34 question. When something in your pipeline fails, do you check the step that reported the error, or the last step that anyone actually verified?

Day 34 down. Sixty-six to go.

Top comments (0)