Una risposta giusta può arrivare dalla fonte sbagliata

Il task sembrava semplice: leggere una card di GitHub Projects, verificarne stato e dipendenze e non usare il browser. La risposta dell’agente era ordinata e convincente. Riportava un titolo coerente, lo stato Ready, l’obiettivo, i requisiti e perfino le attività fuori scope. Per alcuni minuti ho pensato che il nuovo collegamento remoto funzionasse.

Poi ho aperto il registro delle chiamate. Non comparivano gli strumenti di GitHub Projects. Comparivano invece una ricerca dei repository installati e la lettura di una issue attraverso il connettore GitHub già disponibile nell’app. Il numero della issue coincideva con quello richiamato nella card, quindi il contenuto sembrava corretto anche se il percorso non lo era.

Questo è il tipo di errore più insidioso nei workflow agentici: non un fallimento evidente, ma un risultato plausibile costruito con una fonte diversa da quella autorizzata. Se avessi valutato soltanto il testo finale, avrei certificato un’integrazione che non era mai stata usata.

La traccia che ha smentito il risultato
Richiesto:
GitHub Projects MCP -> card -> campi della board

Osservato nei log:
codex_apps -> github.search_installed_repositories_v2
codex_apps -> github.fetch_issue

Assente:
projects_list
projects_get

Issue e Project card descrivono oggetti diversi

Una issue contiene titolo, corpo, commenti, label e altri dati del repository. Una card di Projects può puntare a quella issue, ma aggiunge uno strato di pianificazione: Status, Priority, Iteration, date, campi personalizzati e posizione nella board. Leggere l’issue non equivale quindi a leggere la card.

Nel mio caso la parola Ready era presente nel Markdown dell’issue. L’agente l’ha riportata come stato della board senza interrogare il campo Status. Anche le dipendenze provenivano dal testo: non era stato controllato se le card collegate fossero davvero Done, Blocked o ancora in lavorazione.

La distinzione diventa importante quando un agente deve sincronizzare il lavoro. Il testo di una issue può descrivere l’intenzione; la board rappresenta lo stato operativo corrente. Confondere i due livelli può produrre aggiornamenti prematuri o blocchi ignorati.

Issue
Elemento del repository usato per descrivere lavoro, bug o discussioni. Il suo Markdown può citare uno stato senza essere la fonte del campo Status.
Project card
Elemento inserito in un GitHub Project. Espone campi della board e può collegarsi a una issue, una pull request o una draft issue.

Perché il falso positivo sembrava così credibile

L’agente non aveva inventato tutto. Aveva trovato un oggetto reale con lo stesso riferimento numerico e aveva estratto informazioni pertinenti. La sostituzione della fonte gli permetteva di completare il task invece di dichiarare che lo strumento richiesto mancava.

La vecchia skill spiegava bene come interpretare Ready, In Progress, Blocked e Done. Impediva transizioni ottimistiche e richiedeva evidenze dal codice. Non specificava però quale canale usare per leggere la board. Quando GitHub Projects MCP non era disponibile, restava aperta una scorciatoia: browser o connettore GitHub generico.

La correzione non è stata chiedere all’agente di “fare più attenzione”. Ho reso osservabile il vincolo: strumenti Projects obbligatori, nessuna sostituzione e arresto esplicito se la capacità non compare nella sessione.

  • Il contenuto letto era autentico, ma apparteneva alla issue.
  • Il riferimento numerico rendeva plausibile la corrispondenza.
  • La skill regolava gli stati, non la provenienza dei dati.
  • Il risultato finale nascondeva il mancato uso del server MCP.

Connettore, MCP e skill risolvono problemi differenti

Un connettore GitHub generico può essere perfetto per repository, issue e pull request. Il toolset Projects remoto espone invece operazioni dedicate alla board. La skill non aggiunge nessuno dei due: descrive il comportamento che l’agente deve adottare quando quegli strumenti sono disponibili.

La formula “MCP dà i poteri, la skill stabilisce la costituzione” funziona come promemoria, ma ha un limite importante. Una skill è un contratto operativo, non una barriera di sicurezza. Se browser e connettori alternativi restano disponibili, la robustezza aumenta soltanto quando anche configurazione, allowlist dei tool e approvazioni riducono tecnicamente le strade possibili.

Il prompt della singola task completa il quadro: specifica l’operazione autorizzata, per esempio una lettura o l’aggiornamento di un solo campo. Capacità, governance e autorizzazione non sono sinonimi.

GitHub Projects MCP
Rende disponibili strumenti specifici per elencare Projects, leggere card e campi e, quando autorizzato, sincronizzarli.
Skill
Definisce il percorso operativo atteso, le verifiche e il comportamento in caso di capacità mancanti. Non avvia né autentica il server.
Prompt della task
Autorizza un’operazione circoscritta in quella sessione. Non dovrebbe ampliare silenziosamente i permessi permanenti.

Dal server locale in Docker al toolset remoto

La prima configurazione usava il GitHub MCP Server locale in un container. Funzionava, ma introduceva una domanda poco elegante per un’attività quotidiana: “quindi devo tenere Docker acceso ogni volta che Codex deve leggere la board?”. Non necessariamente.

GitHub ospita un server MCP remoto compatibile con Streamable HTTP. L’endpoint generale è api.githubcopilot.com/mcp; il percorso /x/projects espone soltanto il toolset Projects, mentre il suffisso /readonly rimuove le operazioni di scrittura. Per questo caso il toolset dedicato riduce rumore e capacità non necessarie.

Passare al remoto elimina il runtime locale, non elimina autenticazione, permessi o verifiche. La disponibilità del servizio è esterna e Codex deve comunque ricevere un token valido nel proprio ambiente. Prima di cambiare configurazione conviene controllare il nome del server esistente con codex mcp list e rimuovere soltanto la voce che hai identificato.

Verifica e prima configurazione read-only
codex mcp list

# Rimuovi solo la vecchia voce identificata, se esiste
codex mcp remove github-projects

codex mcp add github-projects   --url https://api.githubcopilot.com/mcp/x/projects/readonly   --bearer-token-env-var GITHUB_PAT_TOKEN

Scegliere token e permessi senza allargare il perimetro

Gli strumenti projects_list e projects_get dichiarano come scope OAuth richiesto read:project; projects_write richiede project. La guida GitHub per l’API Projects documenta gli stessi scope per query e mutazioni con un personal access token classic. Aggiungi repo soltanto se devi accedere a contenuti di repository privati e la prova dimostra che è necessario.

GitHub raccomanda i fine-grained PAT quando sono supportati, ma documenta ancora un limite per i Projects posseduti da un account personale. Per un Project user-owned questo tutorial usa quindi un PAT classic con scadenza breve e soli scope necessari. Per un Project di organizzazione verifica invece i permessi Projects del token, le policy, l’eventuale approvazione e l’SSO: le organizzazioni possono limitare o vietare i PAT classic.

Un token non concede diritti che l’utente non possiede già, ma resta una credenziale equivalente a una password. Se appare in screenshot, configurazione, cronologia o log condivisi, revocalo e creane uno nuovo. Non limitarti a cancellare l’immagine.

  • Sola lettura: read:project.
  • Scrittura dei campi: project.
  • Repository privati: valuta repo soltanto se realmente richiesto.
  • Scadenza breve, nome riconoscibile e revoca quando l’integrazione non serve più.
  • Project di organizzazione: verifica policy sui PAT e autorizzazione SSO.

Conservare il PAT nel Portachiavi e passarlo all’app

I comandi seguenti sono specifici per macOS. Il Portachiavi conserva il segreto; launchctl lo espone all’ambiente della sessione grafica da cui verrà avviata una nuova istanza di Codex. launchctl non è lo storage permanente e la variabile può scomparire dopo logout o riavvio.

Il comando security permette di lasciare -w come ultima opzione e inserire il valore al prompt, evitando di scrivere il PAT nella riga di comando. Per popolare l’ambiente, recuperalo poi in una variabile temporanea, passalo a launchctl e cancellala immediatamente.

Dopo il setenv chiudi davvero l’app con Cmd+Q e riaprila. Le sessioni già inizializzate non acquisiscono automaticamente nuovi server o nuove variabili.

Portachiavi macOS e ambiente della sessione
# Il comando chiede il PAT senza inserirlo nella cronologia
security add-generic-password   -a "$USER"   -s "codex-github-projects-mcp"   -U   -w

GITHUB_MCP_PAT="$(security find-generic-password   -a "$USER"   -s "codex-github-projects-mcp"   -w)"

launchctl setenv GITHUB_PAT_TOKEN "$GITHUB_MCP_PAT"
unset GITHUB_MCP_PAT

if [ -n "$(launchctl getenv GITHUB_PAT_TOKEN)" ]; then
  echo "Token presente nell'ambiente"
else
  echo "Token assente"
fi

Fare lo stesso su Windows con PowerShell e DPAPI

Su Windows evita di salvare il PAT come variabile User o Machine: quei valori persistenti vengono scritti nel Registro. Puoi invece acquisire il token come SecureString e salvarne una rappresentazione cifrata con ConvertFrom-SecureString. Senza una chiave esplicita, PowerShell usa DPAPI: il file può essere decifrato soltanto dallo stesso account Windows sulla stessa macchina.

Crea prima il file cifrato in una cartella locale dell’utente. Il comando Read-Host -AsSecureString non inserisce il PAT nella cronologia. Il file non deve finire in OneDrive, Git o un archivio condiviso: DPAPI protegge il contenuto a riposo, non autorizza la distribuzione del file.

Quando serve, ricostruisci il SecureString, convertilo temporaneamente nella stringa richiesta dalla variabile di processo e avvia Codex dalla stessa PowerShell. I processi figli ereditano le variabili del processo padre. Il blocco finally azzera il buffer nativo e rimuove GITHUB_PAT_TOKEN dalla sessione PowerShell; l’istanza di Codex già avviata conserva la propria copia fino alla chiusura.

Salvare il PAT cifrato con DPAPI
$TokenDir = Join-Path $env:LOCALAPPDATA "DottorLone"
$TokenFile = Join-Path $TokenDir "codex-github-projects-mcp.txt"

New-Item -ItemType Directory -Force -Path $TokenDir | Out-Null

Read-Host "Incolla il PAT GitHub" -AsSecureString |
  ConvertFrom-SecureString |
  Set-Content -Path $TokenFile -Encoding UTF8

Write-Host "Token cifrato salvato per questo utente e questo PC."

Caricare il token e avviare Codex su Windows

Per la CLI, lascia codex nel blocco try. Per l’app desktop, sostituiscilo con Start-Process e il percorso reale dell’eseguibile installato sul tuo PC. Non copiare un percorso trovato online: individua l’app locale e mantieni le virgolette se contiene spazi.

La verifica controlla soltanto che la variabile sia valorizzata. Non eseguire Write-Output $env:GITHUB_PAT_TOKEN e non incollare l’output diagnostico in una issue. Se la sessione PowerShell viene chiusa prima di avviare Codex, la variabile scompare e va ricreata dal file cifrato.

I comandi MCP restano uguali su PowerShell: codex mcp list, remove e add accettano lo stesso endpoint remoto e lo stesso nome di variabile usati su macOS.

Sessione PowerShell per Codex CLI
$TokenDir = Join-Path $env:LOCALAPPDATA "DottorLone"
$TokenFile = Join-Path $TokenDir "codex-github-projects-mcp.txt"

$SecurePat = Get-Content -Path $TokenFile | ConvertTo-SecureString
$Bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePat)

try {
  $env:GITHUB_PAT_TOKEN =
    [Runtime.InteropServices.Marshal]::PtrToStringBSTR($Bstr)

  if ([string]::IsNullOrEmpty($env:GITHUB_PAT_TOKEN)) {
    throw "Token assente"
  }

  codex
  # Per l'app desktop, usa invece:
  # Start-Process "<PERCORSO_CODEX_EXE>"
}
finally {
  [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($Bstr)
  Remove-Item Env:GITHUB_PAT_TOKEN -ErrorAction SilentlyContinue
}

L’errore in env_http_headers che impediva l’autenticazione

Codex supporta due meccanismi distinti. bearer_token_env_var legge un PAT grezzo dalla variabile indicata e costruisce Authorization: Bearer. env_http_headers associa invece il nome di un header al nome di una variabile d’ambiente; non trasforma automaticamente un PAT nel formato bearer.

Nella configurazione errata il token era finito a destra di una chiave chiamata GITHUB_PAT_TOKEN. Codex lo interpretava come nome di variabile da cercare e, soprattutto, non veniva definito l’header Authorization. Oltre a non autenticare il server, il segreto restava scritto nel file.

env_http_headers non è sbagliato in assoluto: potrebbe associare Authorization a una variabile che contiene già l’intero valore Bearer. In questo caso è però più semplice e meno ambiguo usare l’opzione dedicata.

Configurazione errata e configurazione corretta
# ERRATO: <TOKEN> finisce nel file e non configura Authorization
[mcp_servers.github_projects.env_http_headers]
GITHUB_PAT_TOKEN = "<TOKEN>"

# CORRETTO: il file contiene soltanto il nome della variabile
[mcp_servers.github-projects]
url = "https://api.githubcopilot.com/mcp/x/projects/readonly"
bearer_token_env_var = "GITHUB_PAT_TOKEN"
required = true
enabled_tools = ["projects_list", "projects_get"]

Dimostrare che Codex sta usando davvero Projects MCP

Dopo il riavvio apri il pannello MCP dell’app oppure usa /mcp nella TUI. Il server deve risultare attivo e, nella fase read-only, devono comparire projects_list e projects_get ma non projects_write. Se il server non inizializza, required = true rende il problema esplicito invece di lasciarlo passare inosservato.

La prima task deve essere di sola lettura e deve nominare il canale richiesto. Poi controlla il registro delle attività: non cercare soltanto una risposta corretta, cerca le chiamate projects_list e projects_get. Per leggere i valori della card, passa field_names, per esempio Status e Priority, oppure gli ID in fields. Se non specifichi nessuno dei due, il server restituisce soltanto il titolo dell’item. La risoluzione per nome è comoda, ma un nome inesistente produce un errore strutturato e non va sostituito con una deduzione dal Markdown.

Non condividere alla cieca l’output di codex mcp get. Una redazione tramite espressione regolare può intercettare i formati di token più comuni, ma non è una garanzia completa. Ispeziona il risultato localmente e non pubblicarlo se contiene header statici o valori inattesi.

Prompt di verifica copiabile
Usa esclusivamente GitHub Projects MCP.
Non usare browser, connettori GitHub alternativi o il testo delle issue
come sostituto dei campi della board.

1. Elenca il Project indicato.
2. Leggi la card indicata richiedendo almeno field_names: Status e Priority.
3. Riporta eventuali dipendenze soltanto se sono rappresentate da campi
   o collegamenti restituiti dal Project; non dedurle dal testo dell'issue.
4. Non modificare nulla.
5. Se projects_list o projects_get non sono disponibili, fermati
   e indica esattamente quale capacità manca.

Abilitare la scrittura soltanto dopo il test

Quando le letture sono ripetibili, puoi sostituire l’endpoint read-only con /x/projects. Verifica che il token abbia lo scope project e limita gli strumenti esposti ai tre necessari. La modalità di approvazione writes chiede conferma per gli strumenti che il server dichiara mutativi.

Il primo test di scrittura non deve usare una card casuale. Scegli un elemento identificato, annota il valore iniziale e autorizza la modifica di un solo campo. Rileggi poi la card dal Project e confronta il nuovo valore. Se il task richiede Status, non autorizzare modifiche a titolo, descrizione o altre card.

L’allowlist riduce la superficie di quel server, ma non disabilita automaticamente browser o connettori forniti da altri plugin. Se il processo richiede isolamento forte, rimuovi o disabilita quei canali per la sessione oltre a dichiararne il divieto nella skill.

  1. Conferma che la lettura read-only restituisca Project, campi e card attesi.
  2. Sostituisci l’endpoint e riavvia completamente Codex.
  3. Controlla che projects_write sia comparso e che le scritture richiedano approvazione.
  4. Autorizza una sola modifica esplicita su una card scelta.
  5. Rileggi il campo dal Project e conserva nel log prima, chiamata eseguita e dopo.
Passaggio controllato alla scrittura
codex mcp remove github-projects

codex mcp add github-projects   --url https://api.githubcopilot.com/mcp/x/projects   --bearer-token-env-var GITHUB_PAT_TOKEN

# In ~/.codex/config.toml, nella tabella del server:
required = true
enabled_tools = ["projects_list", "projects_get", "projects_write"]
default_tools_approval_mode = "writes"

Scrivere nella skill un vero comportamento fail closed

Dopo la modifica alla skill, l’agente ha smesso di restituire risultati rassicuranti quando gli strumenti Projects mancavano. Si è fermato e ha dichiarato la capacità assente. Non era una regressione: era la prima risposta coerente con il processo richiesto.

La regola deve nominare strumenti, operazioni consentite e comportamento di arresto. “Usa GitHub” è troppo vago; “usa projects_list e projects_get, non sostituirli e fermati se mancano” è verificabile. Per le scritture, projects_write deve comparire soltanto quando la task le autorizza.

Fail closed non significa bloccare ogni lavoro. Significa evitare che l’agente trasformi un problema di configurazione in una deduzione, una lettura parziale o un cambiamento non tracciabile.

Estratto concettuale della skill
## Access the board

- Access Project cards and board state exclusively through
  GitHub Projects MCP.
- Use projects_list and projects_get for normal read operations.
- Use projects_write only for synchronization explicitly authorized.
- Never fall back to a browser or another GitHub access channel.
- Never infer board fields from issue Markdown.
- If GitHub Projects MCP is unavailable, stop and report
  the missing capability.

La lezione riutilizzabile: controllare il percorso

Da questo incidente mi porto via una regola semplice: con un agente non devo verificare soltanto cosa ha concluso, ma anche da dove arrivano i dati e quale strumento ha prodotto l’evidenza. Più la risposta sembra plausibile, più è facile dimenticare questa seconda verifica.

Il server MCP remoto ha eliminato la dipendenza quotidiana da Docker. La skill ha reso esplicito il canale ammesso. L’endpoint read-only, l’allowlist e le approvazioni hanno aggiunto limiti tecnici. Nessuno di questi livelli sostituisce gli altri.

Questa guida usa endpoint, opzioni Codex e nomi dei tool verificati il 19 agosto 2026. Prima di applicarla, ricontrolla le fonti ufficiali e prova prima una lettura su un Project non critico.

PROSSIMO PASSO

Prova su un caso reale, ma piccolo

Parti da una email, un riassunto o un confronto semplice. Controlla sempre dati personali, date, numeri e decisioni importanti.

Torna a AI pratica