FAQ: A Free Shell Is Not Your GitLab Runner
I keep seeing one shortcut in review threads. Someone debugs a GitLab job on a free shell. Then they treat that shell like the project runner. That shortcut feels fast when the review is late. It also mixes three differ
I keep seeing one shortcut in review threads.
Someone debugs a GitLab job on a free shell.
Then they treat that shell like the project runner.
That shortcut feels fast when the review is late.
It also mixes three different machines into one story.
Which machine actually created the pipeline you are judging?
The mix-up
A free shell can hold a copy of your repo.
A free model can explain the YAML you pasted.
GitLab still decides which jobs exist in the pipeline.
I want those three facts kept strictly apart.
If I blur them, I ship guesses instead of evidence.
Have you done that on a Friday deploy?
What this check is
I am not ranking models in this note.
I am not claiming a measured speed or quota.
I am mapping claims to evidence you can inspect.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I treat its free model access as a drafting aid only.
I treat its free server as a scratch shell, not a runner.
Confirm those two availability claims in the current product docs.
I am not stating hardware, limits, or how long they last.
Those details change, and I will not invent them.
Myth: a clone means the runner can fetch
I hear this right after a clean git clone.
The free shell reached the remote, so CI must work.
That conclusion skips the identity question entirely here.
A clone proves one client, one remote, and one moment.
Your runner may use another network path entirely.
It may also use a different credential than you did.
Corrected model
Store the shell result as shell evidence only.
Do not store it as runner evidence in your notes.
Ask who authenticated, and ask from where.
# Scratch shell only. This does not register a runner.
git remote -v
git rev-parse --abbrev-ref HEAD
git rev-parse HEAD
git status --short
If those commands fail, stop and fix the snapshot.
You do not have a trustworthy tree yet.
Why debug rules against a dirty or missing tree?
Myth: the model already evaluated rules
A model can restate your rules in plain speech.
GitLab evaluates those rules when it creates the pipeline.
A chat transcript is not that creation event.
Did the pipeline page actually list the job?
Did the expanded configuration include that same job?
If both answers are no, the model only read text.
Corrected model
Treat every model output as a written hypothesis.
Treat the created pipeline as the only verdict.
I want that verdict from GitLab, not from a paragraph.
Remote include directives make this split much sharper.
GitLab expands includes before any job starts.
A pasted snippet can hide the rest of the graph.
# File inventory. This does not expand remote includes.
find . -type f \( -name '*.yml' -o -name '*.yaml' \) -print | sort
That inventory is a file list, not an expansion.
Remote includes may not exist on this disk.
Did you even fetch the included project first?
Myth: free access means the job token is safe to paste
This myth is the one I refuse to soften.
A job token is created for one running job.
A free chat or free shell is not that job.
I do not paste job tokens into a model prompt.
I do not echo them on a shared server either.
If one already leaked, I treat that value as exposed.
Corrected model
Check variable names, and never print variable values.
Compare the job environment with a redacted probe.
Rotate any secret that touched a log, chat, or ticket.
# Presence probe. Never print secret values.
python3 - <<'PY'
import os
names = [
'CI_JOB_TOKEN',
'CI_COMMIT_SHA',
'CI_PROJECT_PATH',
'CI_PIPELINE_ID',
]
for name in names:
state = 'set' if os.getenv(name) else 'unset'
print(f'{name}={state}')
PY
Protected and masked are not the same switch.
Masked tries to hide a value in job logs.
Protected limits where GitLab will expose a variable.
I will not freeze those rules into a permanent sentence.
Open the current GitLab CI variable docs before you rely on them.
Product behavior moves, and old blog posts lag behind.
Myth: the free server matches image and services
Your job names a container image in the config.
The free server may be a bare host instead.
Those are not the same runtime, so why compare exits?
Service blocks start companion workloads for that job.
A random shell does not start that mesh for you.
Users, DNS names, and ports can all differ.
Corrected model
Use the free server to edit and inventory files.
Use a registered runner to execute the job script.
If you need the image, run that image on purpose.
# Host facts from the scratch shell. Not the job image.
uname -s
uname -r
command -v docker || true
python3 --version
A missing docker binary is only a host fact.
It does not prove the runner lacks Docker.
It proves this shell cannot show you that executor.
Myth: a drafted job is the include graph
I ask a model to draft a deploy job.
The reply contains a script and a stage name.
People commit that block and call the pipeline designed.
The draft never resolved the project include files.
It never applied the workflow rules either.
It never saw your protected branch policy settings.
Corrected model
Keep the draft on a scratch branch first.
Inventory local CI files before you push.
Then read the expanded CI config GitLab shows for that pipeline.
I still do not call that reading a production approval.
Expansion shows the graph that GitLab built.
It still does not execute the job script.
The labels I keep
- A shell result stays labeled as shell evidence.
- A model draft stays labeled as a hypothesis.
- A created pipeline is the only creation verdict.
- A job trace is the only execution verdict.
How I read the claims
I use this table when a review repeats a claim.
The middle column is the evidence I require.
The right column is all a free shell can honestly show.
| Repeated claim | Evidence that would support it | What a free shell can show |
|---|---|---|
| A clone means the runner can fetch | A runner trace with the same remote and identity | One client fetch, if it succeeded |
| The model already evaluated rules | A created pipeline that contains the job | No rule result at all |
| Pasting the job token is a fair test | Nothing; that token is scoped to one job | A leak risk, not a result |
| Host packages match the job image | A job trace from the declared image | Host package names only |
| Drafted YAML is the include graph | Expanded CI config from GitLab | Local file names only |
A split workflow you can repeat
This section is a proposed workflow, not a recorded run.
I am not publishing output from a private project.
Run it on a scratch branch, then delete that branch.
First, record the shell identity without any secrets.
Second, inventory the CI files sitting on disk.
Third, list job-like keys without evaluating any rules.
Fourth, write one claim per line in a log.
Fifth, open the created pipeline inside GitLab itself.
Only then compare a script failure with the host notes.
mkdir -p .ci-scratch
{
echo "when=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "head=$(git rev-parse HEAD)"
echo "branch=$(git rev-parse --abbrev-ref HEAD)"
} > .ci-scratch/shell-context.txt
# Proposed extractor. It lists keys. It does not evaluate rules.
from pathlib import Path
import sys
try:
import yaml
except ImportError:
sys.exit('PyYAML is not installed in this shell')
reserved = {
'stages',
'variables',
'workflow',
'include',
'default',
'image',
'services',
}
for path in sorted(Path('.').rglob('*.yml')):
text = path.read_text(encoding='utf-8')
data = yaml.safe_load(text) or {}
if not isinstance(data, dict):
continue
for key, value in data.items():
if key in reserved or str(key).startswith('.'):
continue
if isinstance(value, dict) and 'script' in value:
print(f'{path}: job={key}')
The extractor looks for a script key under a mapping.
It skips common top-level CI keys on purpose.
It will miss jobs that arrive only through remote includes.
If PyYAML is missing, that is a shell gap.
Install it only on the scratch server you control.
Do not treat a missing import as a pipeline failure.
I keep each claim in four short fields.
Shell evidence and runner evidence stay in separate fields.
A missing runner field means the claim is still unproven.
claim: runner can fetch this remote
shell_evidence: clone succeeded as the scratch user
runner_evidence: missing
verdict: unproven
Questions before I trust a green shell
Did this shell register itself as a project runner?
Did GitLab create the pipeline after the draft?
Did any secret value leave the job environment?
If I cannot answer those, I keep the verdict open.
A tidy prompt is not a job trace.
A tidy host note is not a trace either.
Limitations
This workflow does not register a GitLab runner.
It does not expand remote include files for you.
It does not prove how protected variables are exposed.
A free server can be restarted, reused, or inspected.
Do not put production secrets on that disk.
Do not treat its disk as GitLab cache or artifacts.
Free model access can misread YAML and still sound sure.
I keep the model on the drafting side of the line.
GitLab remains the source for pipeline creation.
I have not published timings, model names, or capacity numbers.
The operator supplied availability, not a benchmark result.
If a current doc disagrees with this note, follow the doc.
Who should not use this
Skip this if you need a compliance audit trail.
Skip this if the job image is private and unavailable.
Skip this if the repo holds secrets you cannot redact.
Also skip it when the failure is already in a job trace.
Read that trace before you open another chat.
A free shell will not reconstruct the executor for you.
Where the free tools still help
I still draft the claim log with the free model.
I still collect the file inventory on the free server.
Then I stop, and I verify job creation inside GitLab.
That is the only handoff I trust from this setup.
If you want the same split, start from the table.
Leave the runner verdict to a real job trace.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.